1. 程式人生 > >C#語言struct結構體適用場景和注意事項

C#語言struct結構體適用場景和注意事項

C#中struct結構體是一個特殊的存在,值型別棧內拷貝。struct和class定義上有些相似,區別主要是值型別和引用型別的區別。Winform中涉及到原生代碼的地方大量使用了struct,這很大程度上是為了程式碼移植的需要,不能作為我們寫程式碼的規範參考。我們有時感覺結構比較簡單的類改為struct可能會提高效能,但這種感覺在絕大多數情況下其實是錯誤的。那麼我們自己在編寫程式碼的時候究竟在什麼情況下適合定義struct而不是class呢?

選用struct的原則

考慮 定義struct而非class,如果型別的例項很小而且通常存活期都很短,或者一般都嵌入到其它物件中使用

避免

定義struct 除非 型別滿足以下全部特徵:

  • 邏輯上表達了一個單一值,類似基本資料型別(int, double)
  • 例項大小低於16位元組
  • 不可改變
  • 不會被頻繁裝箱

個人總結了一些使用struct的建議:

  • 對於初學者或者一般情況,請使用class不要考慮struct。當程式需要考慮效能而進行優化的階段再考慮struct問題
  • 定義struct時,儘量作為私有型別或內部型別,不要公開
  • struct的屬性不要定義公開的set方法,也就是不可改變。公開的都是隻讀,只能在構造時賦值。
  • 使用struct管理非託管資源時,定義Free方法,使用時一定要在恰當時機呼叫Free。千萬不要想著去實現IDisposable介面。如果覺得不安全,那就改用class吧!
  • 如果需要呼叫原生代碼而迫不得已,才可以無視其它原則而選用struct

struct的效能

選用struct可以在一些特定條件下改善程式效能,但請注意,沒有“銀彈”能夠在所有情況下解決所有問題。

struct一般用於一些結構簡單,可以用單一值概念描述的型別。同時,型別的存活期應該不會太長。struct無需建立即可使用,也沒有垃圾回收問題。struct壓根就不在GC堆記憶體中分配,而是直接在棧記憶體中分配。在使用struct時都會複製到當前棧記憶體中,就像其它值型別一樣。以上這些特性只能說和class在使用上會有差異,需要注意。但說不上是優點還是缺點,取決於用法和具體情況。另外,struct不存在併發競爭問題,多執行緒安全,這應該算是優點了。

一種已知情況可以用struct來優化程式,就是struct型別的陣列(注意是陣列不是List,至於基於雜湊的集合不好說)。struct陣列在物理上一定是一個連續的記憶體塊。如果是引用型別,則物理上一般是分配指標來指向引用的例項,此時陣列的記憶體塊不能涵蓋所有要訪問的資料。而struct陣列在這種情況下所有會用到的資料都在陣列的實體記憶體之中包含,可以直接訪問到,無需通過GC堆記憶體的物件引用來反覆的間接查詢。同時,如果例項數量非常多時,使用struct陣列還能避免大量分散在GC堆中的物件例項,從而減輕GC壓力。

此時,對struct陣列中的下標訪問不會造成複製(List的下標訪問則會),直接記憶體定位效率很高。

int id = structArray[i].Id;

注意,struct欄位不可變會很有幫助,如果需要修改欄位內容,通過ref方法。
定義:

public static void SetId(ref structType target, int value) 
{
    target.Id = value; 
}

使用:

SetId(ref structArray[i], 100);

實際上很多情況下,struct反而會拖慢我們的程式。由於值型別在使用上的複製特性,定義一個龐大的struct在絕大多數情況下效能會比引用型別要糟糕。因為每次使用到struct時都會在棧中複製一份新例項,複製來複制去的,如果struct的定義的欄位比較多佔用很多位元組的話,複製的成本就會很高。這也是為什麼微軟給出的準則中有一條:“當型別定義大於16位元組時不要選用struct”。

struct是不可變的!

首先,一個struct描述了一個單一值,類似於int, char這樣的基本型別,要改變他應當是對整體重新賦值來改變。struct的所有公開的屬性、欄位都應該是用於獲取這個單一值的一些特徵的,這從概念上就杜絕了可賦值的屬性這樣的定義。

其次,由於struct是值型別,分配在棧記憶體中或者是擁有struct型別的引用型別物件中,任何時候對struct的訪問都會訪問原始struct的副本,因此對struct屬性的修改實際上是在修改原始struct的副本。除非你將修改後的struct例項重新賦值回去,否則原始struct是不會改變。這一特性同樣適用於函式方法的引數是struct的情況。

當然,要直接改變原始struct也是有辦法的,那就是使用ref型別的的方法引數來直接改變原始值。但這就需要定義一個專門的方法,通過struct的屬性來訪問時仍然會有上述問題。

舉例來說,如果struct定義了帶有set方法的屬性,那麼在方法內作為區域性變數確實是可以改變的。但是這其中會有陷阱的存在。比如將struct傳入一個方法內,除非引數定義了ref否則方法內的任何修改不會影響原始的變數。另一種情況,某個類的一個成員是struct,如果通過類的屬性訪問修改struct的某個成員,是不會成功的,修改的實際上是棧內的副本,此時只能是將修改後的值重新賦值回去才能生效。

綜上,無論是從概念上還是從實踐的角度,struct的成員都應當是只讀的。要做到這一點,首先將所有Field的可訪問性設定為不公開,公開的只有屬性的get方法。

如果你出於程式碼簡(tou)潔(lan)的考慮設計了一個struct含有可改變的成員,必須將這個struct定義為私有,以保證可控。公開的struct必須宣告成員只讀,才能保證使用者在所有情況下獲得預期的結果。

用struct管理原生代碼的釋放問題

用struct管理原生代碼時,注意定義釋放方法,而使用時要在恰當時機去明確呼叫釋放方法。

struct不允許宣告覆蓋預設的無參構造方法,也沒有析構方法。這是因為struct本身就是一份棧記憶體,無需new新的例項,也無需去釋放。

但如果struct內部使用了本地資源,這時本地資源的釋放就成了問題。對於object的class型別,我們可以定義實現IDisposable介面,在使用時用using程式碼塊來建立例項。但是對於struct來說,千萬不要。因為在using的時候使用的是struct的副本,而記憶體中可能存在很多很多struct的副本。這種情況下,Dispose的邏輯應當非常可靠才能避免重複釋放的問題。

實際上,用struct來管理本地資源的情況強烈推薦要將struct定義為私有或內部,作為一個公開型別的內部實現。這樣可以保證所有使用的例項都能夠被幹淨釋放,避免記憶體洩漏。