微軟基礎類庫(Base Class Library)團隊已經完成了.NET不可變集合的正式版本,但不包括ImmutableArray。與其一起發布的還包括針對其它不可變對象類型的設計指南。
如果你需要在多個線程中安全地共享集合,并且允許每個線程在需要時對其內容進行改變。這種場景就是不可變集合所設計的初衷。只讀集合在使用時需要復制集合中的全部內容,而新的不可變集合可以以一種更高性能的方式從一個現有集合中進行創建。
使用不可變集合需要特別當心,因為你很容易錯誤地寫成“list.Add(item)”,而正確的方法是“list = list.Add(item)”。甚至編譯器也可能產生類似的錯誤,這也是為什么不可變集合不支持構造函數的原因。考慮以下代碼:
list = new ImmutableList<int> {1, 2, 3};
在編譯后會產生以下代碼:
temp = new ImmutableList(); temp.Add(1); temp.Add(2) temp.Add(3) list = temp;
由于3次Add方法的結果都被丟棄,最終整個集合包含的項數目為0,而不是期望中的3。
不可變對象指南
Immo Lendwerth建議,當你在創建自己的不可變對象時,在其中加入適當的WithXxx方法。對簡單的對象來說,為每一個屬性創建一個WithXxx方法即可。當屬性值需要變化時,該方法會返回當前對象的一個拷貝。
如果某屬性代表了一個結合,那么這種模式就需要一點變化。以下這段代碼來自Immo的發布聲明:
class Order { public Order(IEnumerable<OrderLine> lines) { Lines = lines.ToImmutableList(); } public ImmutableList<OrderLine> Lines { get; private set; } public Order WithLines(IEnumerableOrderLine> value) { return Object.ReferenceEquals(Lines, value) ? this : new Order(value); } }
如你所見,WithLines方法可接受任意IEnumerable。因此你可以傳遞一個新創建的ImmutableList對象,或者是某個LINQ表達式的結果。這種方式已經足以滿足需求了,不過他還建議提供某些輔助方法:
class Order { //... public Order AddLine(OrderLine value) { return WithLines(Lines.Add(value)); } public Order RemoveLine(OrderLine value) { return WithLines(Lines.Remove(value)); } public Order ReplaceLine(OrderLine oldValue, OrderLine newValue) { return oldValue == newValue ? this : WithLines(Lines.Replace(oldValue, newValue)); } }
ImmutableArray被移除
由于性能方面的原因,ImmutableArray從最終的發布版本中被移除。其原因是:為了滿足內存性能指標,ImmutableArray必須設計成一個值對象,并且為了保持值對象的語義,ImmutableArray的默認實例必須表現為一個空數組形式。不幸的是,為了達到這一點,對空值的檢測(null check)會使得C#無法移除對數組邊界的檢測,而這一點是為達到良好CPU性能的一個重要考慮事項。
由于ImmutableArray類對于Roslyn編譯器項目非常重要,設計者曾考慮刪除會導致性能問題的空值檢測功能,但又因此產生了另外的問題,Immo這樣寫道:
由于所有的值類型都有一個自動產生的默認構造函數,它會將該值類型初始化為它的默認狀態,而ImmutableArray<T>的默認值是空,它的底層數組實現則為null。因此,AddRange方法的實現會因為NullReferenceException的產生而崩潰。
這一問題還表現在其它一些地方,由于ImmutableArray<T>實現了某些集合接口(例如IEnumerable和IReadOnlyList),因此你可以把它傳遞給某些接受這種接口的方法。由于這種接口引用是非空的,使用者在調用它的方法或者屬性時不會考慮到有可能產生NullReferenceException。
基礎類庫團隊并未放棄這個項目,他們還在研究其它設計方式,以爭取讓ImmutableArray重新亮相。
文章列表