2011年4月23日 星期六

3-5. 常數 / 變數 與 mutable / Immutable

之前我們曾提及 val 與 var,val 大約可比擬其他語言的 const。
val 在 Scala 非常被強調,盡量可以使用 val 的地方,請盡量使用。

接觸過函數式語言的洗禮後,你應該知道 Scala 是不喜歡變數這種東西的,因為不管是為了支援 concurrency,或是降低副作用(Side-Effect),使用變數都不是一種好習慣。

這個原則在你平常的程式撰寫也是同樣,就算你不使用 Scala,也應謹守「盡量不使用變數」的原則。「用修改變數值來作為控制程式的手段」這個習慣真的不好,儘快改變掉這習慣,對你在學習 Scala,或撰寫好的程式都會有很大的幫助。

除了 val 外,還有一個看起來很類似的東西,Immutable Data Type。

所謂 Immutable Data Type,就是這個 Data Type裡面的 field不能被更改。Mutable的英文字是可更改的,Immutable 的意思就是不可更改的。
所謂 Immutable 就是不能被更改,表示一旦設定值(其實應該叫被 initial 值之後),以後不能被改變。

「這個 Immutable 好像和 val 一樣啊?為何有了 val 又要有 Immutable?」

不,val 與 Immutable 是不相同的東西。

我們首先釐清一個觀念,在 Java 中所有的 object (或 Scala 中,大部分的 object)都是 reference type。reference type 的意思是,變數只儲存一個 reference 到真正儲存資料的記憶體,變數並非擁有真正的資料。我們姑且可以把 reference type 當成一個 pointer,也就是, Java 的 object 變數,只是一個 pointer,point到真正的資料。

val 指的是「該變數(或叫該常數好了)不能再被變動」。所以 val 的變數不能被重新 assign 新的值,但並沒有限定被指定到的地方不能該新值。

讓我們舉例說明
class People(name: String) // 宣告一個 class 叫 People,有一個 name 的 data field。現在可能暫時有點看不懂,請先跳過一下
val p1 = new People("John") // 產生一個 People instance,他的 name 叫 John,注意 p1 是 val (常數)
p1.name = "Jason" // 修改 p1.name,這是正確的,雖然 p1 是 val
p1 = new People("Jason") // 重新 assign 一個 object 給 p1,但由於 p1 是 val,所以 compiler 出現錯誤

第三個 statement 可以正常工作,你看出問題了嗎?
沒錯,雖然 p1 是 val,但 p1 可以變動 name 欄位。若有兩個 thread,一個讀取 p1.name,一個更改 p1.name,不是有可能產生 race condition 嗎?

確實沒錯,所以才會有 Immutable Data Type 這個東西, Immutable Data Type 表示該值不能該動,也就是不希望透過變數更改到欄位值。

「有沒有實例呢?」
有,其實你也用過, Java裡面的 String,就是一個 Immutable 的 data type。讓我們看例子
var s1 = "A" //設定 s1 為 "A" 字串
var s2 = s1 //設定 s2 指到 s1,s1, s2 此時都指到 "A" 字串
s1 = "B" //設定 s1 為 "B" 字串
println(s2) // 這裡會印出 "A"
上述的 statements 是正確的。
最後 statemenet 要印出 s2,結果會是"A"。
原因在於第三個 statement,雖然更改了 s1 的指向指到 "B",但此時 s2 並沒有改變,仍舊指到舊的 "A",所以印出 "A"。
String 這個 class 的 instance 一旦被 create 之後,instance 裡面的值就不可被改變,雖然 s1 更改了,但此時系統會重新 create 一個新的 String instance 給 s1,此時 s2 所指的 instance 完全沒有更改。

到此,我們應該很清楚
1. val 所訂定的是變數本身,說明的是,該變數不能被 reassign。
2. Immutable 指的是,所指到的資料結構不能被更改。
val 與 immutable 兩者所訂定的對象不同。若要完整支援 concurrency,val 配上 immutable 是最合適的。

「除 String 外,還有什麼東西是 Immutable?」

其實,Scala 幫大家準備好了。我們常用的資料結構,List、Set、Map,在 Scala 世界都有兩份,一份是 mutable,另一份是 immutable,供大家使用。

使用 mutable 的版本,就和在 Java 的使用情況相同,允許我們在同一個 instance 中增加、修改、或減少該 instance 的資料。使用這種版本,在 multi-thread 的情況下,programmer 需要自行掌握資料的完整性 (data consistence)。

使用 immutable 的版本,instance 一旦 initial 完畢,該 instance 的資料就不會再被改變。如果呼叫該 instance 的 add 或 delete 等修改資料的 method,系統會 create 一個新的 instance,並把原來 instance 資料 copy 一份過來,然後做修改,最後返回這個新的 instance。

「create 一個新的 instance,並把原來 instance 資料 copy 一份過來,然後做修改」,這幾個動作在新的 instance 初始資料時一次完成。當修改資料的 method 完成時,所使用到的資料結構已經是完全新的,舊的資料結構完全沒有被更動到。

舉例說明,若有兩個變數 x1, x2 指到同一個 immutable 資料 d1,其中變數 x2 呼叫修改資料 d1 的 method m(),此時系統的執行狀況如下
1. 產生一個新的 immutable 資料 d2,d2 的初始值使用 d1,以及 method m()
2. 讓 x2 指到新的資料 d2
3. d1 沒有任何改變,x1 也沒有任何改變,仍然是指到 d1

由上述的動作,我們可以瞭解 x1 並沒有變化,x2 的改變只改變到它自己的部份。所以就算 x1 與 x2 在不同的 thread,彼此也不會互相影響,不會有 race condition 的情況發生,這對撰寫concurrency 程式是一大保障。

Immutable 的資料結構帶來程式穩定的保證,但它的代價是「當修改的 method 呼叫時,重新複製一份資料的記憶體浪費,以及複製的時間」。

Mutable 與 immutable 這兩種版本, Scala 的 default 使用 immutable 版本。請不用擔心無法使用 mutable 版本,Scala 這裡的 default 表示你不特別加以指定時會使用 immutable。你隨時可以使用 mutable 版本,甚至在同一個 class 中使用兩個不同版本的資料結構,一個是 mutable 另一個是immutable。

val 在 Scala 中非常被強調, immutable 也被同樣強調,主要原因是要讓開發 concurrency 程式不再是那麼痛苦的事。

沒有留言:

張貼留言