探索c#之不可變資料型別

發表於2015-09-06

不可變物件

不可變(immutable): 即物件一旦被建立初始化後,它們的值就不能被改變,之後的每次改變都會產生一個新物件。

c#中的string是不可變的,Substring(0, 6)返回的是一個新字串值,而原字串在共享域中是不變的。另外一個StringBuilder是可變的,這也是推薦使用StringBuilder的原因。

當儲存值18的記憶體分配給age變數時,它的記憶體值也是不可以被修改的。

此時會在棧中開闢新值2賦值給age變數,而不能改變18這個記憶體裡的值,int在c#中也是不可變的。

我們例項化MutableContact賦值給mutable,隨後我們可以修改MutableContact物件內部欄位值,它已經不是初始後的值,可稱為可變(mutable)物件。

可變物件在多執行緒併發中共享,是存在一些問題的。多執行緒下A執行緒賦值到 Name = “大毛” 這一步,其他的執行緒有可能讀取到的資料就是:

很明顯這樣資料完整性就不能保障,也有稱資料撕裂。我們把可變物件更改為不可變物件如下:

使用時只能通過Contact2的建構函式來初始化Name和Address欄位。Contact2此時即為不可變物件,因為物件本身是個不可變整體。通過使用不可變物件可以不用擔心資料完整性,也能保證資料安全性,不會被其他執行緒修改。

 

自定義不可變集合

我們去列舉可變集合時,出於執行緒安全的考慮我們往往需要進行加鎖處理,防止該集合在其他執行緒被修改,而使用不可變集合則能避免這個問題。我們平常使用的資料結構都是採用可變模式來實現的,那怎麼實現一個不可變資料結構呢!以棧來示例,具體程式碼如下:

 

  • 入棧時會例項化一個新棧物件
  • 將新值通過建構函式傳入,並存放在新物件Head位置,舊棧物件放在在Tail位置引用
  • 出棧時返回當前棧物件的Tail引用的棧物件

使用方法如下:

每次Push都是一個新物件,舊物件不可修改,這樣在列舉集合就不需要擔心其他執行緒修改了。

 

Net提供的不可變集合

不可變佇列,不可變列表等資料結構如果都自己實現工作量確實有點大。幸好的是Net在4.5版本已經提供了不可變集合的基礎類庫。 使用Nuget安裝:

使用如下,和上面我們自定義的幾乎一樣:

使用Net不可變列表集合有一點要注意的是,當我們Push值時要重新賦值給原變數才正確,因為push後會生成一個新物件,原a1只是舊值:

NET提供的常用資料結構

  • ImmutableStack
  • ImmutableQueue
  • ImmutableList
  • ImmutableHashSet
  • ImmutableSortedSet
  • ImmutableDictionary<K, V>
  • ImmutableSortedDictionary<K, V>

不可變集合和可變集合在演算法複雜度上的不同:

 

不可變優點

  • 集合共享安全,從不被改變
  • 訪問集合時,不需要鎖集合(執行緒安全)
  • 修改集合不擔心舊集合被改變
  • 書寫更簡潔,函式式風格。 var list = ImmutableList.Empty.Add(10).Add(20).Add(30);
  • 保證資料完整性,安全性

 

不可變物件缺點

不可變本身的優點即是缺點,當每次物件/集合操作都會返回個新值。而舊值依舊會保留一段時間,這會使記憶體有極大開銷,也會給GC造成回收負擔,效能也比可變集合差的多。

跟string和StringBuild一樣,Net提供的不可變集合也增加了批量操作的API,用來避免大量建立物件:

我們來對比下可變集合、不可變Builder集合、不可變集合的效能,新增新物件1000W次:

比較程式碼如下:

另外一個缺點比較有趣,也有不少人忽略。 由於string的不可變特性,所以當我們使用string在儲存敏感資訊時,就需要特別注意。

比如密碼 var pwd=”mushroomsir”,此時密碼會以明文儲存在記憶體中,也許你稍後會加密置空等,但這都是會生成新值的。而明文會長時間儲存在共享域記憶體中,任何能拿到dump檔案的人都可以看到明文,增加了密碼被竊取的風險。

當然這不是一個新問題,net2.0提供的有SecureString來進行安全儲存,使用時進行恢復及清理。

相關文章