Unity中的结构体(C#)

发表于2018-07-18
评论0 1.5k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594
结构体和类很像,但又完全不同如果大家对结构体不甚了解,但又想用结构体,也许你不知道传引用和传值的差别,那么本篇文章对结构体的介绍和使用一定能帮到你。

Unity中的结构体

既然这个系列是为了Unity而学习C#的,那先来了解一下,那些已经使用了结构体的地方吧。
  • Vector2, Vector3 和 Vector4
  • Rect
  • Color和Color32
  • Bounds
  • Touch
尤其,各种形式的Vector(2-4)使用的非常广泛。你会发现它们被用于存储各种信息,从变换的位置、旋转、大小,到刚体的速度,或者触摸、点击的屏幕位置。

什么是结构体

结构体是一种复合数据类型。它和类很像,你可以用相同的方式定义域和方法。下面的例子定义了一个结构体和一个类,它们几乎是一样的
public struct PointA
{
    public int x;
    public int y;
}
public class PointB
{
    public int x;
    public int y;
}

在这个例子中,最显著区别就是关键字——“struct”而不是“class”。其他区别包括:
  • 结构体不能从基类继承,但类可以
  • 结构体不能有无参构造函数
  • 在构造函数结束之前,所有的结构体域都必须被赋值
  • 结构体是传值,而类的实例是传引用
最后一点,对我来说也是最重要一点。“值”类型和“引用”类型之间有很显著的差别,它会影响到应该何时及如何使用它们。

引用类型

当说到类的实例是传引用时,实际过程是,先获取一个指针,它指向对象在内存中的地址,然后传递这个指针。这很重要,因为一个类的实例,实际上可能很大,包含了很多域甚至其他对象。在这种情况下,赋值和传递整个实例可能非常影响性能,这就为什么要用传地址来替代。

引用类型在“堆”上分配,在调用“垃圾回收”时被清理。垃圾回收是一个自动的过程,但是它很慢,通常会降游戏的帧率。基于这个原因,最好不要频繁创建对象并让它们超出作用域。下面的例子就是一个大忌:
//最好别这样做
void Update ()
{
    //在Update循环中创建局部作用域的类实例(每帧调用)
    List<GameObject> objects = new List<GameObject>();
    //假设对这个对象列表执行了一些操作(可能是填充、迭代等)
    for (int i = 0; i < objects.Count; ++i) 
    {
    }
    //当方法结束时,对象列表超出作用域,有时有这种需求
    //执行垃圾回收
}

值类型

说到传值时,实际过程是,对这个变量进行全克隆/拷贝,然后传递这个副本,原始值不变。结构体就是值类型,它是传值的。这意味着,结构体是理想的小型数据结构。

值类型在“栈”的分配,这意味着它们的内存很容易被回收,它们不受“垃圾回收”的影响。和Update循环例子中的引用类型不同,创建值类型是完全合理的,它们超出作用域也不必担心帧率下降或内存问题。下面的例子就是完全合理的:
//这样是可以的
void Update ()
{
    //创建一个值类型的局部变量——结构体
    Vector3 offset = new Vector3 (UnityEngine.Random.Range (-1, 1), 0, 0);
    //对它执行操作
    Vector3 pos = transform.localPosition;
    pos += offset * Time.deltaTime;
    transform.localPosition = pos;
    //当超出作用域,你的结构体内存很容易被回收
}

陷阱

人们很容易像使用类的实例一样使用结构体,但是因为它是值传递,可能会经常遇见一些陷阱。看看下面的例子:
using UnityEngine;
using System.Collections;
public class Demo : MonoBehaviour
{
    public Vector3 v1;
    public Vector3 v2 { get; private set; }
    void Start ()
    {
        v1.Set(1,2,3);
        v1.x = 4;
        v2.Set(1,2,3);      // ** (Note 2)
        v2.x = 4;           // * (Note 1)
        Debug.Log(v1.ToString());
        Debug.Log(v2.ToString());
    }
}
* (Note 1)这一行会导致程序无法编译。你会看到错误提示“错误CS1612:不能修改’Demo.v2’返回的值类型。考虑将该值存储到临时变量中”。编译器保护你远离一个逻辑错误(这个我稍后会解释),并建议你先创建一个新的结构体,修改新的结构体,然后将它赋值给你原本想要修改的那个。

** (Note 2)更为危险,因为它会编译通过并运行,但实际上它并未生效。

如果代码编译通过并运行,应该会看到如下输出结果:
(4.0, 2.0, 3.0)
(0.0, 0.0, 0.0)

这可能并不是你预期的。所以,发生了什么?C#为‘v2’自动创建了一个隐藏的backer属性。当你使用getter时(通过简单地引用‘v2’),C#提供了一个backer的副本,而不是真正的backer——记住这是因为结构体是传值而不是传引用。在Note2这一行,实际是,你获得了一个backer的副本,在这里修改了副本,之后这些信息立即丢失了,因为它们并没有被赋值回去。

下面的例子也一样——它说明了引用类型和值类型的概念,通常是如何被忽视并导致问题的。这里我们持有一个列表的引用,它持有一个Vector3的引用。
usingUnityEngine;
usingSystem.Collections;
usingSystem.Collections.Generic;
public class Demo : MonoBehaviour
{
    voidStart ()
    {
        List<Vector3> coords = new List<Vector3>();
        coords.Add( new Vector3(0, 0, 0) );
        coords[0].Set(1, 2, 3);
        coords[0].x = 4;
        //错误CS1612(参考上例,注释掉本行编译)
        Debug.Log(coords[0].ToString());        //输出(0.0, 0.0, 0.0),并非预期值!
    }
}

相比之下,下面的例子将会按照预期运行(或者至少有了上一个例子作为恐吓或混淆你应该有所预期)
usingUnityEngine;
usingSystem.Collections;
public class Foo
{
    public Vector3 pos;
}
public class Demo : MonoBehaviour
{
    voidStart ()
    {
        Foo myFoo = new Foo();
        myFoo.pos.Set(1, 2, 3);
        myFoo.pos.x = 4;
        //没有编译错误
       Debug.Log(myFoo.pos.ToString());
       //输出(4.0, 2.0, 3.0),和预期一致
     }
}

为什么这个例子正常而另一个不是呢?答案就是,因为我们使用的是‘myFoo’的引用——而不是对象域的引用。这个对象直接持有了结构体的值(作为一个域),并直接修改它,并不会产生错误。

是否应该让Vector3作为Foo的一个属性,而不是一个域(即使是一个指定了backing的域)?这是个问题——看看下面的例子:
usingUnityEngine;
usingSystem.Collections;
public class Foo
{
    public Vector3 pos { get{ return _pos; } set{ _pos = value; } }
    private Vector3 _pos;
}
public class Demo : MonoBehaviour
{
    void Start ()
    {
        Foo myFoo = new Foo();
        myFoo.pos.Set(1, 2, 3);
        myFoo.pos.x = 4;
       //错误CS1612(参考上例,注释掉本行编译)
       Debug.Log(myFoo.pos.ToString());
       //输出(0.0, 0.0, 0.0),并非预期值!
    }
}
这些问题很多是可以缓解的,如果你能够将结构体视为“不可变”的(这意味着绝不改变任何域的值),或将它们定义为不可变的(如果它只是你的结构体)。

总结

本课介绍了结构体,并比较了何时、何处及为何要使用它而不是类。还展示了一些结构体的限制和陷阱,但也有它们的好处。正确地使用结构体,它是非常重要高效的工具,把它加入到你的编程中吧。
原文链接:https://theliquidfire.wordpress.com/2015/03/23/structs/
原文作者:Jonathan Parham

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台