CSharp - 何時使用結構?

  显示原文与译文双语对照的内容

在 C# 中應使用結構而非類? 我的概念模型是結構用於項目時僅僅是值的集合類型。 一種邏輯上將它們全部放在一起的一種方法。

我在這裡遇到了這些規則 ( 緩存的 ):

  • 結構應表示單個值。
  • 結構的內存佔用應該少於 16位元組。
  • 創建后不應更改結構。

這些規則是否工作結構在語義上意味著什麼?

时间:

源引用的op有可信度。但是微軟——對結構使用的立場是什麼? 我從微軟尋求一些額外的學習,這是我的發現:

如果類型的實例很小且通常是短的或者通常嵌入在其他對象中,則考慮定義一個結構而不是類。

除非類型具有以下所有特徵,否則不定義結構:

  1. 它邏輯上表示一個值,類似於基元類型( 整數,double等) 。
  2. 它的實例大小小於16位元組。
  3. 它是不可變的。
  4. 它將不必頻繁被裝箱。

微軟一貫違反那些規則

好,#2 和 #3 無論如何。 我們喜愛的字典有 2個內部結構:


[StructLayout(LayoutKind.Sequential)]//default for structs
private struct Entry//<Tkey, TValue>
{
//use Reflector to see the code
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
 IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
 IDictionaryEnumerator, IEnumerator
{
//use Reflector to see the code
}

'JonnyCantCode.com'源得到 3 4——相當forgivable自 #4 可能不會是一個問題。 如果你發現自己正在打拳,請重新考慮你的架構。

讓我們看看為什麼微軟會使用這些結構:

  1. 每個結構 EntryEnumerator 代表單個值。
  2. 速度
  3. Entry 從不作為字典類之外的參數傳遞。 進一步的調查表明,為了滿足IEnumerable的實現,字典使用了 Enumerator 結構,每次枚舉器被請求時它都會使用它。
  4. 字典類內部。 Enumerator 是公共的,因為字典是可以枚舉的,並且必須對IEnumerator介面實現具有同等的可以訪問性- 比如 IEnumerator getter 。

更新——此外,意識到結構實現一個介面時,枚舉器一樣,是把實現類型,結構成為一個引用類型,並移動到堆中。 在字典類內部,枚舉器仍然是一個值類型。 但是,只要一個方法調用 GetEnumerator(),就會返回一個 reference-type IEnumerator

我們這裡沒有看到的是任何保持結構不變或者保持一個實例大小僅 16位元組或者更少的實例的嘗試或者證明:

  1. 上結構中的任何內容都未聲明為 readonly - 不可變
  2. 這些結構的大小可以超過 16位元組
  3. Entry 具有未定的生存期( 從 Add(),到 Remove()Clear() 或者垃圾收集) ;

而且。。4.兩種結構都存儲TKey和 TValue,我們都知道它們是引用類型( 附加的額外信息)

儘管散列鍵,字典是快,部分是因為比引用類型實例化一個struct更快。 這裡,我有一個 Dictionary<int, int>,它存儲 300,000個隨機整數,順序遞增鍵。

容量:312874
MemSize: 2660827位元組
已經完成大小調整:5毫秒
填充總時間:889毫秒

容量: 內部數組必須調整大小之前可用的元素數。

MemSize: 通過將字典序列化到MemoryStream並獲取位元組長度( 滿足我們的目的) 來確定。

已經完成的調整大小: 將內部數組從 150862元素調整到 312874元素所需的時間。 當你認為每個元素都是通過 Array.CopyTo() 複製的時候,這並不是太糟糕。

填補總時間: 誠然傾斜由於伐木和 OnResize 事件我添加到源;然而,仍然令人印象深刻的填補 300k整數而調整 15次操作。 好奇的是,如果我已經知道了容量,那麼填充的總時間是多少? 13ms

那麼,如果 Entry 是一個類? 這些時間或者指標真的有那麼多不同?

容量:312874
MemSize: 2660827位元組
已經完成大小調整:26毫秒
填充總時間:964毫秒

很明顯,大差別在於調整大小。 使用容量初始化字典的任何差異? 沒有足夠的關注。。 12ms

發生了什麼,因為 Entry 是一個結構,它不需要像引用類型那樣初始化。 這既是價值類型的美麗又是 bane 。 為了使用 Entry 作為引用類型,我必須插入以下代碼:


/*
 * Added to satisfy initialization of entry elements --
 * this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i <prime ; i++)
{
 destinationArray[i] = new Entry( );
}
/* *********************************************** */

我必須將 Entry的每個數組元素初始化為引用類型的原因可以在 MSDN中找到: 結構設計 。簡而言之:

不提供結構的默認構造函數。

如果結構定義了默認構造函數,當結構的數組被創建時,公共語言運行庫在每個數組元素上自動執行默認的構造函數。

某些編譯器,如 C# 編譯器,不允許結構具有默認構造函數。

它實際上是很簡單,我們將借用 Asimov的機器人三定律 :

  1. 結構必須是安全
  2. 結構必須有效地執行它的函數,除非這違反了規則 #1
  3. 結構在使用期間必須保持不變,除非需要它的破壞才能滿足規則 #1

。從這個帶走什麼呢: 簡而言之,要負責使用值類型。 它們是快速和高效的,但如果沒有正確維護( 例如 ),就會產生許多意想不到的行為。 無意的副本) 。

當不需要多態時,需要值語義,並希望避免堆分配和相關的垃圾收集開銷。 然而,警告是結構( 任意大) 比類引用( 通常一個機器詞) 要昂貴得多,所以類在實踐中可能會更快。

我不同意原始文章中給出的規則。 以下是我的規則:

1 ) 在數組中存儲時使用結構進行性能。 ( 另請參閱什麼時候為結構)? )

2 ) 在將結構化數據傳遞到/c+ +的代碼中需要它們

3 ) 除非需要結構,否則不要使用它們:

  • 它們的行為不同於"普通對象"( 引用類型 ) 和作為參數傳遞的參數,這可能導致意外行為;如果查看代碼的人不知道他正在處理一個結構,這尤其危險。
  • 無法繼承它們。
  • 將結構作為參數傳遞比類昂貴。

當你想要值語義而不是引用語義時,使用結構。

編輯

不知道為什麼人downvoting這但這是一個有效的點,和之前op澄清他的問題,這是最基本的一個結構體的基本原因。

如果你需要引用語義,你需要一個類而不是一個結構。

除了"這是一個值"回答,使用結構體的一個特定的場景是當你知道有一組數據導致垃圾收集的問題,和你有很多對象。 例如一個大型的List/數組的Person實例。 這裡的自然隱喻是一個類,但是如果你有大量的long-lived Person實例,它們會阻塞GEN-2並導致GC停頓。 如果權證的場景,一個潛在的方法是使用一個 ( 不是 List ) 人結構體數組, 換句話說,Person[] 現在,在GEN-2中沒有數百萬個對象,你在 LOH ( 我假設這裡沒有字元串等- 換句話說,沒有任何引用的純數值) 上有一個單獨的塊。 這對GC的影響微乎其微。

處理這些數據很麻煩,因為數據可能是over-sized的結構,而且你不想一直複製fat值。 但是,直接在數組中訪問它並不複製結構- 它是 in-place ( 與 List 索引器相對應,後者複製) 。 這意味著有大量的索引工作:


int index =.. .
int id = peopleArray[index].Id;

注意,保持值本身不變會在這裡有所幫助。 對於更複雜的邏輯,請使用帶有by-ref參數的方法:


void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

同樣,這是 in-place - 我們沒有複製值。

在非常特定的場景中,這種策略可能非常成功;但是,它是一個相當高級的scernario,應該只在你知道自己正在做什麼和為什麼要做的情況下嘗試。 默認的是一個類。

結構對於數據的原子表示是很好的,因為這些數據可以被代碼多次複製。 克隆一個對象通常比複製一個結構更昂貴,因為它涉及分配內存,運行構造函數和釋放/垃圾回收當完成時。

C# 語言規範:

1.7 結構

類一樣,結構是可以包含數據成員和函數成員的數據結構,但與類不同,結構是值類型,不需要堆分配。 結構類型的變數直接存儲結構的數據,而類類型的變數存儲對動態分配對象的引用。 結構類型不支持user-specified繼承,並且所有結構類型都隱式繼承自類型對象。

結構對於具有值語義的小數據結構尤其有用。 複數,坐標系中的點或者字典中的key-value對都是結構的好例子。 使用結構而不是類對小型數據結構的使用會在應用程序執行的內存分配數量上產生很大差異。 例如以下程序創建並初始化一個 100點數組。 通過實現作為類的點,101個獨立的對象是數組的instantiated—one,其中一個是 100元素的一個。


class Point
{
 public int x, y;

 public Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
}

class Test
{
 static void Main() {
 Point[] points = new Point[100];
 for (int i = 0; i <100; i++) points[i] = new Point(i, i);
 }
}

另一種方法是使點成為結構。


struct Point
{
 public int x, y;

 public Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
}

現在,只有一個對象是instantiated—the一個用於 array—and 。點實例在數組中存儲 in-line 。

結構構造函數用新運算符調用,但這並不意味著內存正在被分配。 結構構造函數不是動態地分配對象並返回對它的引用,而是返回struct值本身( 通常位於堆棧上的臨時位置),然後根據需要複製這個值。

有了類,兩個變數就可以引用同一個對象,因此在一個變數上的操作可以影響另一個變數引用的對象。 有了結構,每個變數都有自己的數據副本,而對一個操作的操作則不能影響另一個。 例如由以下代碼Fragment生成的輸出取決於點是類還是結構。


Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

如果點是類,輸出是 20,因為a 和引用相同的對象。 如果點是一個結構,輸出是 10,因為 a.x.的賦值創建了一個值的副本,而這個副本不影響到的後續賦值

上一個例子突出了結構的兩個限制。 首先,複製整個結構通常比複製對象引用效率低,因此賦值和值參數傳遞的代價比引用類型更昂貴。 其次,除了引用和輸出參數之外,不可能創建對結構的引用,這在許多情況下排除了它們的用法。

我認為最好的第一個近似是"從不"。

我認為一個好的第二個近似是"從不"。

如果你渴望性能,考慮它們,但是總是度量。

首先:互操作場景或者需要指定內存布局

第二:當數據與引用指針的大小幾乎相同時。

除了直接使用的valuetypes段的運行時和其他各種pinvoke目的,你應該只使用 valuetypes 2場景。

  1. 當你需要複製語義時。
  2. 需要自動初始化時,通常在這些類型的數組中。
...