2010-07-02 64 views
43

一般我們都知道mutable structs are evil。我也很確定,因爲IEnumerable<T>.GetEnumerator()返回類型IEnumerator<T>,結構立即被裝箱成一個引用類型,比如果它們僅僅是引用類型開始,花費更多。爲什麼BCL集合使用結構枚舉器而不是類?

那麼,爲什麼在BCL泛型集合中,所有枚舉類型都是可變結構?當然必須有一個很好的理由。唯一發生在我身上的是可以很容易地複製結構體,從而將枚舉器狀態保留在任意點。但是將Copy()方法添加到IEnumerator接口本來就不那麼麻煩,所以我不認爲這是一個合理的理由。

即使我不同意設計決定,我希望能夠理解背後的推理。

+0

別人過這個運行相關的頁面: http://stackoverflow.com/questions/384511/enumerator-implementation-use-struct-or-class http://www.eggheadcafe.com/software/ aspnet/31702392/c-compiler-challenge - s.aspx – 2010-07-02 18:59:08

回答

62

事實上,這是出於性能原因。 BCL團隊在決定採用一種可疑和危險的做法來決定採用什麼樣的方式進行研究之前做了批次的研究:使用可變值類型。

你問爲什麼這不會導致拳擊。這是因爲C#編譯器不會生成代碼來將東西裝入IEnumerable或IEnumerator的foreach循環中(如果它可以避免的話)!

當我們看到

foreach(X x in c) 

,我們首先要做的是檢查是否C有一個名爲GetEnumerator方法。如果是這樣,那麼我們檢查它返回的類型是否具有方法MoveNext和屬性current。如果是這樣,那麼foreach循環完全是使用對這些方法和屬性的直接調用生成的。只有在「模式」不能匹配的情況下,我們纔會回頭去尋找接口。

這有兩個理想的效果。首先,如果集合是一個整數集合,但是在泛型類型被髮明之前編寫,那麼它不會承擔拳擊Current對象的值並將其拆箱爲int的裝箱罰款。如果Current是一個返回int的屬性,我們就使用它。

其次,如果枚舉器是一個值類型,那麼它不會將枚舉器裝箱到IEnumerator。

就像我說的,BCL團隊在這方面做了大量的研究,發現絕大多數時候,分配和取消分配的懲罰足夠大,因此值得把它作爲一個值類型即使這樣做可能會導致一些瘋狂的錯誤。

例如,考慮一下:

struct MyHandle : IDisposable { ... } 
... 
using (MyHandle h = whatever) 
{ 
    h = somethingElse; 
} 

你會很正確地期望嘗試變異小時,失敗,的確如此。編譯器檢測到您正在嘗試更改具有掛起處置的內容的值,並且這樣做可能會導致需要處理的對象實際上不會被處置。

現在,假設你有:

struct MyHandle : IDisposable { ... } 
... 
using (MyHandle h = whatever) 
{ 
    h.Mutate(); 
} 

這裏會發生什麼?如果h是一個只讀字段,您可能會合理地認爲編譯器會執行它的操作:make a copy, and mutate the copy爲了確保該方法不會丟棄需要處理的值中的東西。

然而,與我們有關的直覺衝突應該是什麼在這裏發生:無論它是

using (Enumerator enumtor = whatever) 
{ 
    ... 
    enumtor.MoveNext(); 
    ... 
} 

我們希望做一個使用塊內的MoveNext將移動枚舉到下一個一個struct或一個ref類型。

不幸的是,今天的C#編譯器有一個bug。如果您處於這種情況,我們會選擇不一致的策略。今天的行爲是:

  • 如果值類型變量被通過的方法突變是一種正常的地方,然後它通常突變

  • ,但如果它是一個懸掛本地(因爲它是一個閉環在匿名函數的變量或迭代器塊中),那麼本地實際上是作爲只讀字段生成的,並且確保副本上發生突變的設備接管。

不幸的是,該規範很少提供這方面的指導。顯然有些事情因爲我們做得不一致而被打破,但是要做的事情一點都不清楚。

+1

+1這意味着有身邊掠過的(最小的)性能損失的'IEnumerable的',而不是原來的泛型集合 - 在快速釋放模式測試列舉了一個'名單'千萬條目都直接並且當投射到「IEnumerable 」時,我看到2:1的一致時間差(在這種情況下,〜100ms vs〜50ms)。 – 2010-07-02 19:09:04

+0

好的答案,我不知道這種優化 - 但它非常有意義。我覺得有些諷刺的是,我掛你的博客備份我的聲明可變的結構是邪惡的 - 你回答我的問題:) – Eloff 2010-07-02 23:14:42

+0

順便說一句,這是一個醜陋的極端情況,但與可變結構的另一個問題。 – Eloff 2010-07-02 23:32:50

5

在編譯時已知結構體的類型,並且通過接口調用方法很慢,所以答案是:由於性能原因,結構體方法是內聯的。

+0

但是這些都是內部結構,所以這種類型在編譯時是不知道的;所有最終用戶代碼都通過接口訪問它們。 – 2010-07-02 18:53:49

+1

@Stephen:'List .Enumerator'被記錄爲在MSDN中公開... – 2010-07-02 19:04:40

+0

如果你看例如列表 .GetEnunmerator方法(http://msdn.microsoft.com/en-us/library/b0yss765 .aspx)你可以看到它返回列表 ::枚舉器結構。 C#中的foreach循環不直接使用IEnumerable接口,如果類具有GetEnumerator方法,則足夠了。所以枚舉類型在編譯時是已知的。 – STO 2010-07-02 19:05:19