Unity中的结构体(C#)

来源:互联网 发布:平安科技待遇 知乎 编辑:程序博客网 时间:2024/06/03 16:54

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

转自:http://blog.csdn.net/liulong1567/article/details/50678930

原创粉丝点击