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<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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章