结构体和类很像,但完全不同。抱歉:) 如果你不知道结构体,或者你想用结构体,或者你不知道传引用和传值的差别,那么这一课就是为你量身定做。
Unity中的结构体
既然这个系列是为了Unity而学习C#的,那先来了解一下,那些已经使用了结构体的地方吧。
尤其,各种形式的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