簡介
C/C++程序員或多或少都有使用struct的經歷,在C++中struct和class的區別不大,除了默認成員的可訪問性,這點在C#中則截然不同。本文將力圖說明C#中struct和class的區別以及如何正確的使用struct。
為什么需要struct?
眾所周知,在java中並沒有struct的概念,那么C#為何引入struct呢?最基本原因是可以創建值類型的類型,使在托管環境中有更好的性能。
區別於java,C#有值類型和引用類型的概念(java只有引用類型)。引用類型的實例分配在堆上,當對象沒有被引用時被垃圾回收器回收;值類型的實例分配在棧上,當離開其作用域后內存被回收。值類型本身存儲的是值本身,引用類型存儲的是引用,C#語言提供的原始類型除了string類型其它都是值類型。
在C#中struct是值類型,class是引用類型,可以通過enum和struct關鍵字創建值類型的對象。使用值類型可以減少托管堆上對象的數量,從而減少垃圾回收器(GC)的負擔,提高性能,值類型也有明顯的缺點:通過值類型傳遞較大對象的開銷比較昂貴、裝箱和拆箱對性能造成影響。
Classes 和Structs
public struct Employeestruct { private int _fooNumber; public Employeestruct(int fooNumber) : this() { _fooNumber = fooNumber; } public string Name { get; set; } public int Age { get; set; } public string GetMessage() { return string.Format("{0}--{1}", Name, Age); } }
從上面可以看到,struct和class非常相似,不過它們還是有本質的區別,接下來我們就一一分析。
1. Structs 和Inheritance
Structs從System.ValueType繼承而classes繼承於System.Object或從System.Object繼承的其他類型,當然System.ValueType也從System.Object繼承(這不是重點)。Structs可以實現接口,但不能從另一個classes、structs繼承,而且不能作為其他classes的基類。要知道,當你把structs作為接口使用時,就進行一次隱形裝箱,因為接口作為引用類型。請看下面代碼:
struct Foo : IFoo { int x; } IFoo iFoo = new Foo();
Foo的實例被創建並賦值給iFoo(裝箱),當iFoo調用時實際上使用的是Foo裝箱后的實例。
2. Constructors
盡管CLR允許,但是不允許在structs中定義無參的構造函數。對於值類型,編譯器默認情況下不生成默認的構造函數,也不調用默認的構造函數,所以C#編譯器不允許使用者定義無參的構造函數。由於structs不生成默認的構造函數,所以不能初始化字段。如下(錯誤):
Struct MyFoo { int x = 1; }
記住,編譯器把初始化工作放在構造函數中,由於structs沒有默認的構造函數,所以不能初始化字段。
有趣的是,你可以使用下面的語法:
Foo foo = new Foo();
通過之前的章節了解到,盡管初始化foo使用了new操作符,但是structs的實例分配到棧上。new Foo()不會調用無參的構造函數,僅僅是初始化該struct的字段為默認值。
struct Foo { int x; public Foo(int x) { this.x = x; } } Foo foo = new Foo();
注意,我已經重載了構造函數,然而我能夠調用new Foo()。
3. Destructors
Structs不允許定義析構函數,如果你扔就去定義一個析構函數,編譯器會立即提示一個錯誤,不過structs可以實現IDisposable接口。
4. Comparison against null
Structs不能和null進行比較,這點在.net framework2.0已不再是問題(可空類型),關於可空類型,超出本文討論范圍。
5. Readonly關鍵字
對於引用類型,readonly阻止再為變量賦值,但不阻止你修改當前引用對象的狀態。對於值類型,readonly有點類似C++中的const關鍵字,它阻止你修改引用對象,也意味着不能再為他賦值,因為這會導致重新初始化一次。請看下面代碼:
public class MyReferenceType { public int State { get; set; } } public struct MyValueType { public int State { get; set; } } public void TestMethod() { myReferenceType = new MyReferenceType(); // 錯誤 myReferenceType.State = 1234; // Ok myValueType = new MyValueType(); // 錯誤 myValueType.State = 1234; // 錯誤 }
foreach 語句和using語句的變量為隱式readonly,所以如果使用structs,將無法更改其狀態。
何時使用 structs
通過前面的描述已經清楚classes和structs的區別,那我們來看下適合使用structs的場景:
l 實例使用起來像C#的基元類型
l 需要創建大量的、短暫實例(例如在循環體內)
l 實例不需要大量的傳遞
l 不需要從其他類型繼承或不希望被其他類型繼承
l 希望被人操作你實例的副本
不適合使用structs的場景:
l 實例過大,當實例過大時,傳遞實例會有很大的開銷。微軟建議structs的理想大小應該在16bytes以下
l 當回引起裝箱、拆箱操作時。關於裝箱、拆箱超出本文討論范圍