一想到這兩個(gè)的區(qū)別拘荡,大多數(shù)人第一反應(yīng)就是臼节,var 修飾的變量可改變,val 修飾的變量不可改變珊皿;但真的如此嗎网缝?事實(shí)上,var 修飾的對(duì)象引用可以改變亮隙,val 修飾的則不可改變,但對(duì)象的狀態(tài)卻是可以改變的垢夹。例如:
class A(n: Int) {
var value = n
}
class B(n: Int) {
val value = new A(n)
}
object Test {
def main(args: Array[String]) {
val x = new B(5)
x = new B(6) // 錯(cuò)誤溢吻,因?yàn)?x 為 val 修飾的,引用不可改變
x.value = new A(6) // 錯(cuò)誤,因?yàn)?x.value 為 val 修飾的促王,引用不可改變
x.value.value = 6 // 正確犀盟,x.value.value 為var 修飾的,可以重新賦值
}
}
對(duì)于變量的不變性有很許多的好處蝇狼。
一是阅畴,如果一個(gè)對(duì)象不想改變其內(nèi)部的狀態(tài),那么由于不變性迅耘,我們不用擔(dān)心程序的其他部分會(huì)改變對(duì)象的狀態(tài)贱枣;例如
x = new B(0)
f(x)
if (x.value.value == 0)
println("f didn't do anything to x")
else
println("f did something to x")
這對(duì)于多線程(多進(jìn)程)的系統(tǒng)來(lái)說(shuō)尤其重要。在一個(gè)多線程系統(tǒng)中颤专,以下可能發(fā)生:
x = new B(1)
f(x)
if (x.value.value == 1) {
print(x.value.value) // Can be different than 1!
}
如果你只使用 val 纽哥,并且只使用不可變的數(shù)據(jù)結(jié)構(gòu)(即是,避免使用 arrays 栖秕,scala.collection.mutable 內(nèi)的任何東西春塌,等等),你可以放心簇捍,這是不會(huì)發(fā)生的只壳;除非有一些代碼,例如一個(gè)框架暑塑,進(jìn)行反射——反射可以改變不可變的值吼句。
二是,當(dāng)用 var 修飾的時(shí)候梯投,你可能在多個(gè)地方重用 var 修飾的變量命辖,這樣會(huì)產(chǎn)生下面的問(wèn)題:
- 對(duì)于閱讀代碼的人來(lái)說(shuō),在代碼的確定部分中知道變量的值是比較困難的分蓖;
- 你可能會(huì)在使用代碼前初始化代碼尔艇,這樣會(huì)導(dǎo)致錯(cuò)誤;
因此么鹤,簡(jiǎn)單地說(shuō)终娃,使用 val 是安全和增強(qiáng)代碼可讀性的。
既然 val 有這么多的好處蒸甜,那為什么還要使用(或者說(shuō)存在) var 棠耕; 的確,有些編程語(yǔ)言只存在 val的情況 柠新,但是有些情況下窍荧,使用可變性可以大幅度提高程序的執(zhí)行效率。
例如恨憎,對(duì)于一個(gè)不可變的 Queue 蕊退,當(dāng)每次對(duì)隊(duì)列進(jìn)行 enqueue 和 dequeue 操作時(shí)郊楣,將會(huì)得到一個(gè)新的 Queue 對(duì)象,那么 瓤荔,如何處理所有的項(xiàng)目呢净蚤?
下面我們通過(guò)例子進(jìn)行解釋。假設(shè)一個(gè) Int 類型的隊(duì)列输硝,對(duì)隊(duì)列內(nèi)的所有數(shù)字進(jìn)行組合今瀑;例如隊(duì)列的元素有 1,2点把,3橘荠,那么組合后的數(shù)字就是 123。下面是第一種解決方案:采用的是 mutable.Queue
def toNum(q: scala.collection.mutable.Queue[Int]) = {
var num = 0
while (!q.isEmpty) {
num *= 10
num += q.dequeue
}
num
}
上面的代碼易于閱讀和理解愉粤,但存在一個(gè)主要的問(wèn)題是砾医,會(huì)改變?cè)磾?shù)據(jù),因此在調(diào)用 toNum 方法前必須對(duì)源數(shù)據(jù)進(jìn)行拷貝衣厘,避免對(duì)源數(shù)據(jù)產(chǎn)生污染如蚜。這時(shí)一種對(duì)對(duì)象進(jìn)行不變性管理的方法。
接下來(lái)采用 immutable.Queue :
def toNum(q: scala.collection.immutable.Queue[Int]) = {
def recurse(qr: scala.collection.immutable.Queue[Int], num: Int): Int = {
if (qr.isEmpty)
num
else {
val (digit, newQ) = qr.dequeue
recurse(newQ, num * 10 + digit)
}
}
recurse(q, 0)
}
因?yàn)?num 不能被重新分配值影暴,就像在前面的例子中一樣错邦,因此需要使用遞歸。這是一個(gè)尾部遞歸型宙,它的性能很好撬呢。但情況并非總是如此:有時(shí)根本就沒有好的(可讀的、簡(jiǎn)單的)尾遞歸解決方案妆兑。
下面采用 immutable.Queue 和 mutable.Queue 對(duì)代碼進(jìn)行重寫:
def toNum(q: scala.collection.immutable.Queue[Int]) = {
var qr = q
var num = 0
while (!qr.isEmpty) {
val (digit, newQ) = qr.dequeue
num *= 10
num += digit
qr = newQ
}
num
}
這段代碼非常有效魂拦,不需要遞歸,也無(wú)需擔(dān)心是否需要在調(diào)用toNum之前復(fù)制隊(duì)列搁嗓。自然地芯勘,我避免了由于其他用途而對(duì)變量進(jìn)行重用,由于在這個(gè)函數(shù)之外沒有任何代碼可以看到(修改)它們腺逛,所以我不需要擔(dān)心它們的值在代碼的其他地方會(huì)發(fā)生——除非我明確地這么做荷愕。
如果程序員認(rèn)為某種解決方案是最好的解決方案,那么 Scala 就允許程序員這么做棍矛。其他變成語(yǔ)言則沒有這么大的靈活性安疗。Scala (以及任何具有廣泛可變性的語(yǔ)言)的代價(jià)是够委,編譯器在優(yōu)化代碼方面沒有足夠的靈活性荐类。Java的提供解決方案是基于運(yùn)行時(shí)來(lái)優(yōu)化代碼。