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