Unity中的结构体(C#)

结构体和类很像,但完全不同。抱歉:) 如果你不知道结构体,或者你想用结构体,或者你不知道传引用和传值的差别,那么这一课就是为你量身定做。

 

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 objects = new List();
        //假设对这个对象列表执行了一些操作(可能是填充、迭代等)
        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 coords = new List();
            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

你可能感兴趣的:(Unity3d,C#)