var+不可變資料結構 vs val+可變資料結構

fairjm發表於2015-03-18

本文來自 http://www.ituring.com.cn/ 轉截請註明出處


看書的時候偶爾看到就在這裡寫寫想法,當作記錄儲存一時的想法可能表達地會比較奇怪,也許會有不少錯誤歡迎指正。

首先說說val值不可變(等同於java裡的final)。值不可變對於引用型別來說意味著指向的記憶體中的實際物件不變,他只約束了變數只能對應某個物件,但不約束所對應物件內部狀態是否可以發生變化。

而不可變的資料結構意味著在這個結構(物件)的生命週期內,一旦這個物件構建完成,該物件的狀態凍結,不能再進行修改。一般對訪問器做限制,讓修改行為直接拋錯(比如Collections.unmodifiablelist),或者對每個修改行為返回一個新的物件(舊物件作為一個快照留下來)。

對於常見的引用透明,結合上面看(我上面沒寫錯的話QAQ)要使用val + 不可變資料結構(可變資料結構修改內部狀態的話本身就帶副作用,也不符合定義)。

能使用val + 不可變資料結構,我感覺是一種很理想的狀態,比如erlang,但實際中使用的話其實挺麻煩的,erlang中在actor保留狀態是將狀態作為入參傳入,再在最後呼叫的其他方法中傳入新的狀態。對於所有涉及到狀態的操作也通過這個思路,所以我在想elixir中使用的是var+不可變資料結構是否是為了規避這種複雜的寫法。其次,對於JVM,不像Beam對不可變結構有優化(怎麼優化的我不太清楚哈),大量使用不可變資料結構的話勢必會造成比用可變資料結構更多的GC和延遲,這裡又如何權衡呢(話說這一點以前做scala分享的時候被同事質疑過)。

對於不可變對於併發的好處,這邊的不可變應該是指使用不可變結構而不是val。在併發下比較常見的一個情況是暴露出了計算的中間狀態,而不是要麼是初始的要麼是最終的,但使用var+不可變資料結構的話只能保證一次計算是完整的,但無法保證更新不丟失。 簡單的程式碼:

class MutableExample {
  val innerState = collection.mutable.ArrayBuffer[String]()
  def act() = innerState += "1"
      def size = innerState.size()
}

class ImmutableExample {
  var innerState = Vector[String]()
  def act() = innerState = innerState :+ "1"
      def size = innerState.size()
}

畫了兩個草圖: enter image description here enter image description here A、B執行緒同時更新或者其中一個執行緒在更新時另一個執行緒獲取了舊的快照

對於以上程式碼裡的兩個類,在併發環境下導致錯誤的原因不一樣,MutableExample中的 += 程式碼如下: def +=(elem: A): this.type = { ensureSize(size0 + 1) array(size0) = elem.asInstanceOf[AnyRef] size0 += 1 this } 當多個執行緒訪問時 size0會和對應位置下的元素是否已經被賦值的狀態不一致,在array(size0) = elem.asInstanceOf[AnyRef]中發生覆蓋,錯誤發生在內部。
而ImmutableExample,錯誤發生的原因是多個執行緒訪問,在其他執行緒未處理完更新innerState之前有執行緒讀取,導致丟失更新,發生在外部。
但解決方式卻是一樣的,上鎖。但在size()方法中,卻有不同,可變的例子會有size=10但其實已經有11個元素的問題,而不可變結構不會有這種問題,對於訪問,可變資料結構需要加鎖(其實這麼說也有點片面 如果可變結構內部只有一個量在變),而不可變結構不需要。 還有一個優點是作為一個快照,可變資料結構可以被隨意賦值,而不可變資料結構要保證可控,不能讓它被賦值到我們不可控的程式碼裡去。

總的感覺,使用不可變資料結構可以比用可變資料結構更“大膽”地寫程式碼。

綜上,對於使用val+不可變結構來儲存狀態未免實現起來過於麻煩,那麼退一步來說,使用var+不可變資料結構 要比使用 val+可變資料結構來得方便。但問題是不可變資料結構的大量使用可能會對一些平臺(Java)在效能上產生壓力。


... ...一路寫下去 似乎不清楚自己要說什麼但好像又清楚似的 不管啦 就這樣啦... ...
有錯誤歡迎指正.. ..

相關文章