文/玄魂
前言
從 .NET4.0開始,到現在的4.5,我們可以感受得到微軟在並行、多線程、異步編程上帶給開發人員的驚喜。在多線程開發中,無可避免的涉及多個線程共享對象問題,Immutable Object(不可變對象)在保證線程安全方面的重要性被凸顯出來。簡單不可變對象,比如單例,我們可以很輕松的創建並維護,一些復雜對象,對象引用或者集合對象的場景 ,創建和維護不可變對象變得困難了很多。微軟在這方面也做了很多努力,目前看最令我欣喜的就是Immutable Collections了。如果您了解函數式編程,那么對此肯定不會陌生。
當然除了線程安全,不可變集合還有其他的應用場景,本文也會有所涉及。
筆者最近研讀了幾篇MSDN Blog中關於Immutable Collections的英文博文(在文后會給出鏈接)。我看到的博客中的代碼和我下載的版本有些出入,我根據自己的理解重新整理,改編成此文,水平有限,歡迎討論。
1.1 Immutability OBJECT簡單分類
真正的不可變對象
這類對象只能在編譯時賦值,在C#中const類型的變量屬於這個類型。
一次初始化對象
運行時初始化一次,之后再也不會被改變。典型的單例對象就屬於這一類。
淺度不變和深度不變
以C#為例,對象本身是Static ReadOnly類型,但是這不能保證該對象內部成員是線程安全的,這類對象具有淺度不變性,如果能保證對象本身、對象內部任何成員或者嵌套成員都具有不變性則該對象具有深度不變性。
顯然,具有深度不變性的對象是理想的線程安全模型。
1.2 安裝和使用
不要誤會安裝的含義,這里是指從Nuget安裝提供不可變集合功能的Dll。
運行環境:vs2012,.NET 4.5
PM> Install-Package Microsoft.Bcl.Immutable -pre
您正在從 Microsoft 下載 Microsoft.Bcl.Immutable,有關此程序包的許可協議在 http://go.microsoft.com/fwlink/?LinkID=272980&clcid=0x409 上提供。請檢查此程序包是否有其他依賴項,這些依賴項可能帶有各自的許可協議。您若使用程序包及依賴項,即構成您接受其許可協議。如果您不接受這些許可協議,請從您的設備中刪除相關組件。
已成功安裝“Microsoft.Bcl.Immutable 1.0.8-beta”。
已成功將“Microsoft.Bcl.Immutable 1.0.8-beta”添加到。。。
這個Preview版本的安裝包包含了如下不可變類型:
· ImmutableStack<T>
· ImmutableQueue<T>
· ImmutableList<T>
· ImmutableHashSet<T>
· ImmutableSortedSet<T>
· ImmutableDictionary<K, V>
· ImmutableSortedDictionary<K, V>
每種類型都繼承自相應的接口,從而保證之后不可變類型的可擴展性。
先以ImmutableList<T>為例,開始我們的不可變集合之旅。
class Program
{
static void Main(string[] args)
{
ImmutableList<string> emptyBusket = ImmutableList.Create<string>();
}
}
注意上面的代碼,我們沒有使用構造函數來初始化ImmutableList<string>集合,而是使用名為ImmutableList的Create方法,該方法返回一個空的不可變集合。使用空集合在某些情況下可以避免內存浪費。
Create方法有7個重載,可以傳入初始化數據和比較器。
下面我們嘗試向這個集合中添加一些數據。
class Program
{
static void Main(string[] args)
{
ImmutableList<string> emptyBusket = ImmutableList.Create<string>();
var fruitBasket = emptyBusket.Add("apple");
}
}
我想您已經看到Immutable Collections和傳統集合的一個區別 了,Add方法創建了一個新的集合。這里我們也可以使用AddRange方法批量添加數據創建新的實例。
1.3 Builders
有時,我們可能更需要對一個集合多次修改才能到達要求。從上面的示例我們知道,每次修改都會創建新的集合,這就意味着要開辟新的內存,並且存在數據拷貝。程序本身的執行效率會下降同時GC壓力會增大。
其實同樣的問題再String類型上也存在,反復的修改字符串值存在同樣的問題,.NET中StringBuilder用來解決這個問題。類似的IMMUTABLE COLLECTIONS也提供了Builder類型。同時我們具有了在迭代的同時修改集合的能力!
下面我們來看看Builder的基本應用:
static void Main(string[] args)
{
ImmutableList<string> fruitBusket = ImmutableList.Create<string>("apple","orange","pear");
var builder = fruitBusket.ToBuilder();
foreach (var fruit in fruitBusket)
{
if (fruit == "pear")
{
builder.Remove(fruit);
}
}
builder.Add("ananas");
fruitBusket = builder.ToImmutable();
foreach (var f in fruitBusket)
{
Console.WriteLine(f);
}
Console.Read();
}
在上面的代碼中,使用ToBuilder方法獲取Builder對象,在最后使用To ToImmutable方法返回IMMUTABLE COLLECTION。這里需要注意的是ToBuilder方法並沒有拷貝資源給新Builder對象,Builder的所有操作都和集合共享內存。也許您要懷疑,既然是共享內存,那么Builder修改數據的時候集合怎么能不變化呢?這是因為ImmutableList的內部數據結構是樹,只需要在更新集合的時候創建一個新的引用包含不同節點的引用即可。內部的實現原理,我會在下一篇博文中繼續探討。上面的代碼既沒有修改原來的IMMUTABLE COLLECTION也沒有拷貝整個集合的內部操作。運行結果如下:
1.4 性能(Performance)
immutable collections 在很多方面,性能優於可變集合。當然性能上的優勢和可變還是不可變的關系並不大,主要原因在於immutable collections內部的數據結構。比如下面的代碼:
private List<T> collection;
public IReadOnlyList<int> SomeProperty
{
get
{
lock (this)
{
return this.collection.ToList();
}
}
}
每次訪問都會引起內存拷貝的操作。
但是如果使用immutable collection就可以避免這個問題:
private ImmutableList<T> collection;
public IReadOnlyList<int> SomeProperty
{
get { return this.collection; }
}
內部原理和對性能的影響放在下一篇博客探討,下面的列表是在算法復雜度層面的對比:
Mutable (amortized) |
Mutable (worst case) |
Immutable |
|
Stack.Push |
O(1) |
O(n) |
O(1) |
Queue.Enqueue |
O(1) |
O(n) |
O(1) |
List.Add |
O(1) |
O(n) |
O(log n) |
HashSet.Add |
O(1) |
O(n) |
O(log n) |
SortedSet.Add |
O(log n) |
O(n) |
O(log n) |
Dictionary.Add |
O(1) |
O(n) |
O(log n) |
SortedDictionary.Add |
O(log n) |
O(n log n) |
O(log n) |
在內存使用方面,Immutable Collections要比可變類型的集合要多,空間換時間,這個世界上沒有兩全其美的事情。
小結
本篇博文只是淺嘗則止,從概念上為您介紹了Immutable Collections的基本定義,簡單應用。
我並沒有拿典型的應用場景來舉例,但是您可以從它們的線程安全,性能,內存使用等特性上權衡使用。如果有機會,我會將我的實際應用場景分享給您。
接下來,在下一篇博客中,我會探討Immutable Collections的內部原理。也許你現在不會使用.NET4.5,但是其內部原理卻是和平台無關的。
參考資料: