簡(jiǎn)介
Swift中有兩種聲明“變量”的方式,這兩種方式分別使用let和var這兩個(gè)關(guān)鍵字。這應(yīng)該是借鑒了Scala,因?yàn)樗鼈兒蚐cala的val和var有相同的作用。let被用于聲明不變量,var被用于聲明變量。不變量的值一旦被定義就不能再改變,變量則可以在聲明之后被隨意賦值。
在其它一些如Java,C這樣的命令式編程語(yǔ)言中也有不變量的概念。但多數(shù)情況下會(huì)被以常量形式使用,常量是靜態(tài)的不變量。在Java中,通常用static和final一起來(lái)定義常量,其中static用于指明其是靜態(tài)的,final用于指明其是不變的。Java中,我們有多種定義常量的方法:接口中定義,類中定義,使用枚舉實(shí)現(xiàn)。這些方法之間的區(qū)別是在何時(shí)何地如何使用static和final。Objective-C,則和C語(yǔ)言一樣,使用const關(guān)鍵字說(shuō)明一個(gè)變量不應(yīng)被改變。
在這類語(yǔ)言中,不變量和變量相比,通常是不尋常的,次一等的概念。如果將一個(gè)名字關(guān)聯(lián)到一個(gè)值,缺省的會(huì)得到一個(gè)變量,而不是不變量。如果,你需要一個(gè)不會(huì)改變,一直和某個(gè)特定值綁定的名字,就需要顯式說(shuō)明它是不變的。例如,在Java中使用final,在C中使用const。這種缺省就是變量的情況,甚至影響了我們的語(yǔ)言。當(dāng)我們需要描述,“聲明用于和某個(gè)值關(guān)聯(lián)的名字”時(shí),我們說(shuō)的是“聲明變量”。但其實(shí),這個(gè)“變量”應(yīng)該加上引號(hào),因?yàn)樗鋵?shí)可能是個(gè)不變量。這和指代不明確性別人時(shí),使用“他”而不是“她”是同一類現(xiàn)象。
“缺省的是變量,如果需要不變量,請(qǐng)顯式說(shuō)明”。這是大多數(shù)命令式編程語(yǔ)言對(duì)變量和不變量的處理方法。這很自然。因?yàn)檫@類語(yǔ)言的設(shè)計(jì)中,大多數(shù)情況下使用的是變量,不變量只是在特殊情況下才需要。Swift(和Scala一樣)則對(duì)這種設(shè)計(jì)做出了修改。從缺省是變量,轉(zhuǎn)變?yōu)檎J(rèn)為變量和不變量的地位是平等的。不變量應(yīng)該更多被提倡和使用。在Swift的語(yǔ)法中,對(duì)這種設(shè)計(jì)思想的體現(xiàn)是:在定義一個(gè)和值關(guān)聯(lián)的名字時(shí),需要明確地使用var或let說(shuō)明它是變量還是不變量。
Swift,和Java,C,Objective-C等語(yǔ)言相比,為何會(huì)有這種對(duì)待不變量的觀點(diǎn)的變化呢?
變量和不變量其實(shí)源于兩種不同編程范式。編程范式是編程語(yǔ)言設(shè)計(jì)者所持有的“世界觀”的反映。
變量來(lái)源于命令式編程范式。這種編程范式將世界視為一系列獨(dú)立的對(duì)象的組合,這些對(duì)象的行為可能會(huì)隨著時(shí)間變化而不斷變化。程序語(yǔ)言中的變量被用于模擬對(duì)象的狀態(tài)。
不變量來(lái)源于函數(shù)式編程范式。這種編程以數(shù)學(xué)函數(shù)為建模核心。試圖將世界抽象成為以一系列數(shù)學(xué)函數(shù)。數(shù)學(xué)函數(shù)中的變量其實(shí)和命令式編程語(yǔ)言中的變量存在著顯著的區(qū)別。基于數(shù)學(xué)的函數(shù)式編程中的變量的概念更接近于命令式編程中的不變量。這在后續(xù)章節(jié)會(huì)詳細(xì)討論。
我們甚至可以通過(guò)對(duì)變量的態(tài)度來(lái)定義命令式編程和函數(shù)式編程:廣泛采用賦值的程序設(shè)計(jì)被稱為命令式程序設(shè)計(jì);不使用任何被賦值的程序設(shè)計(jì)被稱為函數(shù)式程序設(shè)計(jì)。這是因?yàn)椋x值操作使得變量可變。沒(méi)有賦值操作,則變量不可變。
Swift受到了函數(shù)式編程的影響,強(qiáng)化了不變量在語(yǔ)言中位置,鼓勵(lì)不變量的使用。
函數(shù)式編程中的變量
函數(shù)式編程以數(shù)學(xué)函數(shù)為建模基礎(chǔ)。其變量的概念和數(shù)學(xué)中變量的概念是一致的。所以,我們可以先回顧一下數(shù)學(xué)函數(shù)中變量的概念。由于現(xiàn)在絕大多數(shù)程序設(shè)計(jì)語(yǔ)言是命令式的,所以我們通常所說(shuō)的變量是命令式編程中的定義,這和數(shù)學(xué)函數(shù)中的變量并不相同。
在數(shù)學(xué)中,函數(shù)是描述每個(gè)輸入值對(duì)應(yīng)唯一輸出值的這種對(duì)應(yīng)關(guān)系。數(shù)學(xué)函數(shù)中的變量是一個(gè)用于表示值的符號(hào),值是可以是隨意的,也可能是未定的。所以,在數(shù)學(xué)函數(shù)中,某個(gè)符號(hào)我們之所以稱其為變量,是因?yàn)樗梢杂糜诖聿煌闹怠6枰该鞯氖牵寒?dāng)我們用明確的數(shù)值代入函數(shù)運(yùn)算時(shí),變量就擁有了明確的值。而在一次代換過(guò)程中,變量一旦被代換為明確的值,就不會(huì)再次改變?yōu)槠渌怠?shù)學(xué)函數(shù)中不存在這種情況:某一次代換過(guò)程中,某個(gè)變量x一開(kāi)始被代換為2,然后又變?yōu)?。這在數(shù)學(xué)上,沒(méi)有任何意義。
這樣看起來(lái),數(shù)學(xué)函數(shù)中的變量其實(shí)應(yīng)該可以對(duì)應(yīng)程序語(yǔ)言中的不變量:一旦被定義,就不再變化。純粹的函數(shù)式編程語(yǔ)言就完整繼承了這種數(shù)學(xué)上的變量概念。例如,Haskell就沒(méi)有可變量的概念,聲明一個(gè)變量,只能被賦值一次,之后就不會(huì)再變化。而命令式編程語(yǔ)言中,變量被定義之后,仍然能夠隨意被賦予其它的值。
比如我們有一個(gè)簡(jiǎn)單的數(shù)學(xué)函數(shù):
f(x) = 2*x + x * x
如果,我們遵循數(shù)學(xué)函數(shù)對(duì)變量的看法,可以將其翻譯為如下的Swift函數(shù)。這個(gè)程序函數(shù)和上面的數(shù)學(xué)函數(shù),在概念上是等價(jià)的。
func foo(x: Int) -> Int {
return 2*x + x * x
}
當(dāng)然,這個(gè)Swift函數(shù)foo,還有其它現(xiàn)實(shí)方法。函數(shù)的另外一種實(shí)現(xiàn)bar為了展示y是一個(gè)命令式編程里的變量,而稍顯怪異。但它仍然能得到和上面的函數(shù)相同的答案:代入任意相同的x值,兩個(gè)函數(shù)都會(huì)得到相同的返回值。但由于數(shù)學(xué)函數(shù)中不存在y這樣的一開(kāi)始等于某個(gè)值,而后又被賦為另一個(gè)值這樣的命令式編程中的變量概念。所以,我們沒(méi)有辦法將下面這樣的Swift函數(shù)bar還原為一個(gè)概念上一致的數(shù)學(xué)函數(shù)。
func bar(x: Int) -> Int {
var y = 2 * x
y = y + x * x
return y
}
Swift中提供let聲明不變量,更為重視不變性,明確鼓勵(lì)在更多的場(chǎng)合使用不變量。這都是受函數(shù)式編程中變量的不變性的影響。后面會(huì)討論Swift為何會(huì)受到這種影響。
命令式編程中的變量
命令式編程語(yǔ)言中的變量的概念為大多數(shù)程序員所熟悉。我們將其和函數(shù)式編程中的變量做一個(gè)對(duì)比:在函數(shù)式編程中,變量其實(shí)并不可變,這種變量只是一個(gè)代表了某個(gè)值的符號(hào)。而在命令式編程中,由于變量是可變的,變量就不僅僅是簡(jiǎn)單代表一個(gè)值的符號(hào),而是索引了一個(gè)可以保存值的位置,在這個(gè)位置上可以存放不同的值。
我們的世界中每個(gè)對(duì)象都有著自己隨著時(shí)間變化的狀態(tài)。而在不同時(shí)刻,變量可以代表了不同的值,使得變量擁有了時(shí)序上的概念。我們就可以使用變量來(lái)模擬和刻畫現(xiàn)實(shí)世界中的對(duì)象的狀態(tài)。這其實(shí)也是為何會(huì)引入賦值,使得變量可變的原因。
引入賦值的好處
如果使用過(guò)一些函數(shù)式編程語(yǔ)言,就會(huì)發(fā)現(xiàn)部分函數(shù)式編程語(yǔ)言并沒(méi)有完全拋棄賦值。在Scheme中,我們?nèi)匀豢梢杂?set! x 15)這樣的語(yǔ)句為變量賦值,變量將在賦值前后和不同的值關(guān)聯(lián)。為何這些函數(shù)式編程語(yǔ)言沒(méi)有完整地貫徹變量的不變性呢?
函數(shù)式編程語(yǔ)言出現(xiàn)的時(shí)間很早,最早的函數(shù)式編程語(yǔ)言Lisp是歷史第二悠久的高級(jí)編程語(yǔ)言(僅次于Fortran)。但現(xiàn)在函數(shù)式編程語(yǔ)言并沒(méi)有成為絕大多數(shù)程序員的工作語(yǔ)言。為何現(xiàn)今流行的編程語(yǔ)言:C,Java,C++,Python都是命令式編程語(yǔ)言呢?
這是因?yàn)椋?strong>引入賦值,使得變量可變。就引入了一個(gè)簡(jiǎn)單直觀又易于模塊化的程序語(yǔ)言建模方法。這在設(shè)計(jì)大型軟件系統(tǒng)時(shí)是一個(gè)巨大的優(yōu)勢(shì)。
命令式編程的建模思想是一種直觀的世界觀:“世界是由聚集在一起的一系列獨(dú)立的對(duì)象組成的”。但這僅僅是在一個(gè)維度上的描述。另外一個(gè)時(shí)間維度上的描述通常不被提及:“每個(gè)對(duì)象都有著隨時(shí)間變化的狀態(tài)”。綜合來(lái)說(shuō)就是:“世界由對(duì)象組成,對(duì)象都有狀態(tài)”。將這種直觀的世界觀引入程序設(shè)計(jì)所帶來(lái)的好處是,建模更為簡(jiǎn)單了。使用這種思想的編程語(yǔ)言對(duì)于程序員來(lái)說(shuō)也更為簡(jiǎn)單直觀了。那么將一個(gè)實(shí)際問(wèn)題用這種編程語(yǔ)言中的概念來(lái)描述,也就變得更輕松了。因?yàn)椋绦騿T通常能夠?yàn)閷?shí)際問(wèn)題中的事物一一對(duì)應(yīng)地構(gòu)建對(duì)象,并按時(shí)序描述每個(gè)對(duì)象的狀態(tài)。
如果將賦值和局部變量結(jié)合,構(gòu)造帶局部狀態(tài)的對(duì)象,就可以提供一種有利于系統(tǒng)模塊化設(shè)計(jì)的技術(shù)。這是一種強(qiáng)大的設(shè)計(jì)策略,原因在于它的簡(jiǎn)單和直觀。我們可以直接構(gòu)造那些用于模擬真實(shí)物理系統(tǒng)的對(duì)象。對(duì)于問(wèn)題域里的每個(gè)對(duì)象,我們都可以構(gòu)造一個(gè)與之相對(duì)應(yīng)的計(jì)算機(jī)程序里的對(duì)象。如果,我們能把對(duì)象的“狀態(tài)”局限在對(duì)象內(nèi)部,使之成為“局部狀態(tài)”(這其實(shí)就是封裝)。然后,將各自具有“局部狀態(tài)”的對(duì)象組合,這會(huì)是一個(gè)良好的模擬真實(shí)世界的手段。
我們之所以可以使用UML(Unified Modeling Language)來(lái)分析項(xiàng)目需求,是因?yàn)槲覀儗⒃陧?xiàng)目中使用命令式編程語(yǔ)言。從UML這種圖形化的輔助建模方式中,我們可以更明顯地看到如何將真實(shí)世界中的對(duì)象和程序語(yǔ)言中的對(duì)象一一對(duì)應(yīng),如何將真實(shí)世界中的對(duì)象的一個(gè)個(gè)屬性和程序語(yǔ)言中的對(duì)象的變量一一對(duì)應(yīng)。
如果,使用函數(shù)式編程語(yǔ)言,UML將不再能起到任何作用。你需要的是一個(gè)類似將現(xiàn)實(shí)問(wèn)題抽象為數(shù)學(xué)問(wèn)題的過(guò)程。這種數(shù)學(xué)的建模方式對(duì)大多數(shù)人來(lái)說(shuō)可能都會(huì)更為困難一些。
引入賦值的代價(jià)
在函數(shù)式編程中引入賦值,存在著一些爭(zhēng)議。仍然有如Haskell這樣的函數(shù)式編程語(yǔ)言,堅(jiān)持純粹的函數(shù)式編程思想,不使用任何賦值操作(當(dāng)然,仍然有使用不變量難以描述的情況存在。Haskell社區(qū)稱這部分為有副作用的,不純的。這部分代碼會(huì)被限制在Monad中實(shí)現(xiàn))。
也有Swift和Scala這樣的新興語(yǔ)言,重新思考函數(shù)式編程語(yǔ)言中不變性的意義。在語(yǔ)言設(shè)計(jì)中,強(qiáng)調(diào)和重視不變性。
這是因?yàn)闆](méi)有免費(fèi)的午餐。引入賦值,除了上節(jié)所說(shuō)的帶來(lái)了一個(gè)簡(jiǎn)單直觀又易于模塊化的程序語(yǔ)言建模方法之外,也引入了一些缺陷,我們需要為此付出一些代價(jià)。其中一些缺陷使得我們?cè)跇?gòu)建大規(guī)模軟件系統(tǒng)時(shí),遇到了一些難以克服的困難。
更復(fù)雜的計(jì)算模型
為函數(shù)式編程語(yǔ)言引入賦值語(yǔ)句,使得變量可變。看起來(lái)只是多了賦值語(yǔ)法,但其實(shí)這并不是一件簡(jiǎn)單的事情。賦值的引入對(duì)編程語(yǔ)言造成的影響是巨大的:隨著賦值的引入,我們必須為編程語(yǔ)言引入一種更為復(fù)雜的計(jì)算模型。
在沒(méi)有賦值語(yǔ)句之前,純函數(shù)式編程語(yǔ)言可以使用數(shù)學(xué)上的代換模型來(lái)構(gòu)建語(yǔ)言的計(jì)算模型:一個(gè)變量可以安全地被代換為它所代表的表達(dá)式或者值。求值一個(gè)純函數(shù)式編程語(yǔ)言中的函數(shù),和求值一個(gè)數(shù)學(xué)函數(shù)并沒(méi)有什么區(qū)別。你可以認(rèn)為編程語(yǔ)言的運(yùn)行方式和數(shù)學(xué)的運(yùn)算方式是一樣的。這種代換模型其實(shí)是一個(gè)相當(dāng)簡(jiǎn)單的語(yǔ)言模型。
但在引入賦值之后,變量在程序運(yùn)行的某些時(shí)刻代表一個(gè)值,在另一些時(shí)刻代表另外一個(gè)值。代換模型就不再有效了。因?yàn)椋鷵Q模型基于數(shù)學(xué)模型。數(shù)學(xué)上并沒(méi)有在某些時(shí)刻代表一個(gè)值,在另一些時(shí)刻代表另外一個(gè)值的變量概念。如果嘗試對(duì)帶有賦值操作的函數(shù)進(jìn)行代換,會(huì)發(fā)現(xiàn)當(dāng)遇到賦值語(yǔ)句時(shí),代換過(guò)程無(wú)法進(jìn)行下去。因?yàn)樽兞恳呀?jīng)不能被再被看做是某個(gè)值的名字了。此時(shí)的變量以某種方式指定了一個(gè)“位置”,我們可以將任何值存儲(chǔ)在該“位置”。那到底是將哪個(gè)值代入變量呢?在代換模型中,無(wú)法解決該問(wèn)題。
為了解決這個(gè)問(wèn)題,我們引入更為復(fù)雜的環(huán)境模型。變量將維持在我們稱為“環(huán)境”的結(jié)構(gòu)中。環(huán)境包含一系列約束,這些約束將一些變量的名字關(guān)聯(lián)到對(duì)應(yīng)值。在環(huán)境模型中,變量的值將取決于其所處的環(huán)境。程序運(yùn)行過(guò)程中,環(huán)境時(shí)常變化,變量的值也就隨之改變。
引入更復(fù)雜的計(jì)算模型意味著實(shí)現(xiàn)編程語(yǔ)言變得更為困難了。
同一問(wèn)題的復(fù)雜化
相等的判斷
我們拋開(kāi)具體的程序語(yǔ)言討論一下如何判斷對(duì)象相等。在程序語(yǔ)言中,有一種從效果上判斷相同的方法:如果在任意計(jì)算中用一個(gè)對(duì)象替換另外一個(gè)對(duì)象,都不會(huì)改變結(jié)果,那么我們就可以認(rèn)為這兩個(gè)對(duì)象相等。
如果,沒(méi)有賦值操作存在。我們判斷對(duì)象相等會(huì)簡(jiǎn)單一些。例如,在下面例子中,let使Point的實(shí)例變量x和y都成為不變量。p1和p2的x,y相等,而且兩個(gè)點(diǎn)的x,y值都不會(huì)改變。所以,可以認(rèn)為在任何時(shí)候的任何計(jì)算中,p1和p2都是可以相互替換的。我們就可以認(rèn)為p1和p2相等。
struct Point {
let x: Double
let y: Double
}
let p1 = Point(x: 1, y: 2)
let p2 = Point(x: 1, y: 2)
但是,如果我們使用var來(lái)聲明Point的實(shí)例變量。下面例子中的p1和p2相等的結(jié)論就不一定正確了。因?yàn)椋覀兛梢允褂觅x值操作來(lái)改變點(diǎn)的實(shí)際坐標(biāo)了。當(dāng)執(zhí)行p1.x = 2之后,顯然它們就無(wú)法在任何計(jì)算中相互替換了。我們不能認(rèn)為p1和p2相等了。
struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 1, y: 2)
var p2 = Point(x: 1, y: 2)
可以看到在引入賦值之后,判斷兩個(gè)對(duì)象是否相等的問(wèn)題變得更為復(fù)雜了。
別名
在擁有賦值操作后,另外一個(gè)經(jīng)常引起困惑和錯(cuò)誤的是別名問(wèn)題。一個(gè)對(duì)象可以通過(guò)多個(gè)名字訪問(wèn)的的現(xiàn)象稱為別名。下面展示了一個(gè)別名的最簡(jiǎn)單的例子:
class Point {
var x: Double
var y: Double
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 1, y: 2)
var p2 = Point(x: 1, y: 2)
var p3 = p1
p3.x = 2
上面代碼中,p1和p2是兩個(gè)獨(dú)立對(duì)象,p3是p1的別名。這兩組關(guān)系之間有微妙的區(qū)別,我們常常在實(shí)際編程過(guò)程中混淆兩者。p1和p2對(duì)各自的修改互不影響,可以認(rèn)為它們是兩個(gè)獨(dú)立的點(diǎn)。而p1和p3可以認(rèn)為是一個(gè)點(diǎn)。對(duì)其中任何一個(gè)的修改都會(huì)造成另一個(gè)也同樣被修改。如果,我們想在程序中搜索出p1可能被修改的地方,就必須記住,也要檢查那些修改了p3的地方。然而在實(shí)際編程中,特別是在大型復(fù)雜系統(tǒng)中,我們常常會(huì)忘記,或者根本就不知道p3是某個(gè)對(duì)象(這里是p1)的別名。要么,修改了p3,卻不知道也造成了p1的修改。這種副作用常常防不勝防,在編程中經(jīng)常出現(xiàn)。要么,在需要對(duì)修改操作做重新設(shè)計(jì)時(shí),只顧及了p3,而忘記同時(shí)也要修改p1的地方。這種別名常常難以被識(shí)別而被遺忘。
但是,如果沒(méi)有賦值操作,別名造成的困擾就消失了。即使在實(shí)際物理內(nèi)存上,這兩組關(guān)系并不相同:p1和p2指向兩塊不同的內(nèi)存地址,p1和p3指向同一塊內(nèi)存地址。但你仍然可以認(rèn)為p1,p2,p3是相等的對(duì)象。因?yàn)椋跊](méi)有賦值的情況下,它們?cè)谌魏斡?jì)算中都可以相互替換。是否是別名在計(jì)算中并沒(méi)有什么區(qū)別。
值類型和引用類型
也許有人發(fā)現(xiàn):開(kāi)始,我們使用結(jié)構(gòu)體(struct)實(shí)現(xiàn)Point,而后在解釋別名問(wèn)題時(shí)又改用類(class)實(shí)現(xiàn)Point。這是因?yàn)镾wift擴(kuò)大了值類型的使用范圍。
在Java中,可以認(rèn)為原始類型(int,long,float,double,short,char,boolean)是值類型,而其它繼承自O(shè)bject的類型都是引用類型。
而在Swift中,結(jié)構(gòu)體被設(shè)計(jì)成一種值類型。整數(shù),浮點(diǎn)數(shù),布爾值,字符串,數(shù)組和字典在Swift中都是以結(jié)構(gòu)體的形式實(shí)現(xiàn)的,所以,它們也都是值類型。特別是數(shù)組,字典這種常用集合類型也被實(shí)現(xiàn)為值類型,使得值類型在Swift中的使用范圍大大擴(kuò)展了。
值類型在被賦給一個(gè)變量,或者被傳遞給函數(shù)時(shí),實(shí)際上是做了一次拷貝。與值類型對(duì)應(yīng)是引用類型。引用類型在被賦給一個(gè)變量,或者被傳遞給函數(shù)時(shí),是傳遞的是引用。類(class)仍然是引用類型。所以,類實(shí)現(xiàn)的Point會(huì)有別名的問(wèn)題。而值類型不會(huì)有這類別名所帶來(lái)的問(wèn)題。
在下面用結(jié)構(gòu)體實(shí)現(xiàn)Point的例子中,p3不再是p1的別名,而是p1的一個(gè)拷貝。
struct Point {
var x: Double
var y: Double
}
var p1 = Point(x: 1, y: 2)
var p2 = Point(x: 1, y: 2)
var p3 = p1
我們可能會(huì)問(wèn)一個(gè)問(wèn)題:如果每次賦值都進(jìn)行拷貝,是否會(huì)大大增加內(nèi)存開(kāi)銷呢?如果每次賦值都進(jìn)行對(duì)象拷貝,確實(shí)會(huì)增大內(nèi)存開(kāi)銷。Swift的解決方案是:只在值類型發(fā)生改變時(shí)才進(jìn)行拷貝。就上面的結(jié)構(gòu)體實(shí)現(xiàn)的Point的例子而言,var p3 = p1雖然進(jìn)行了賦值,但這時(shí)還并沒(méi)有發(fā)生拷貝操作。這時(shí),p3其實(shí)仍然是p1的別名,它們指向同一個(gè)內(nèi)存地址。直到我們改變p3了,比如執(zhí)行p3.x = 2時(shí),才會(huì)先發(fā)生拷貝,然后在拷貝的副本上進(jìn)行賦值修改操作。這么做當(dāng)然節(jié)省了內(nèi)存開(kāi)銷。而可以這么做的根據(jù)是:沒(méi)有賦值操作時(shí),同一問(wèn)題更簡(jiǎn)單了,別名并不會(huì)帶來(lái)問(wèn)題。在這種沒(méi)有賦值的情況下,值類型和引用類型其實(shí)可以被認(rèn)為是等效的。
擴(kuò)大值類型的使用范圍是Swift減緩別名問(wèn)題的一種方式。另外一種方式,則是我們?cè)诒疚闹幸恢庇懻摰模河捎谫x值操作的引入,使得同一問(wèn)題復(fù)雜化了。那么,即使現(xiàn)在做不到完全去除賦值操作,一定程度上鼓勵(lì)不變性,在需要的環(huán)境中使用不變量,也能緩解這種復(fù)雜性所帶來(lái)的問(wèn)題。
賦值順序
可以舉一個(gè)求階乘的例子來(lái)說(shuō)明,賦值語(yǔ)句的相對(duì)順序?qū)Y(jié)果的影響。
func factorial(n: Int) -> Int {
var product = 1
var i = 1
while i <= n {
product = i * product
i = i + 1
}
return product
}
這個(gè)例子中,如果我們將product = i * product和i = i + 1兩條語(yǔ)句的執(zhí)行順序互換,將會(huì)得到不同的結(jié)果。一般而言,帶有賦值的程序?qū)?qiáng)迫程序員考慮賦值的相對(duì)順序,以保證每個(gè)語(yǔ)句所用的是被修改變量的正確版本。這增加了程序員的負(fù)擔(dān)。使得程序員每次用到賦值時(shí),都需要清楚變量的賦值操作之間的相對(duì)順序。
函數(shù)式編程語(yǔ)言中,由于沒(méi)有賦值,所以根本沒(méi)有這類問(wèn)題。為了對(duì)比,下面例子使用函數(shù)式編程的風(fēng)格再次實(shí)現(xiàn)階乘。在函數(shù)式編程中,一般會(huì)使用遞歸來(lái)代替命令式編程中所用到的循環(huán)結(jié)構(gòu)。這樣風(fēng)格的代碼中,我們無(wú)法體會(huì)到對(duì)于時(shí)序的要求。
func factorial(n: Int) -> Int {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
并發(fā)問(wèn)題
在單線程環(huán)境中,考慮賦值操作的相對(duì)順序?qū)Τ绦蜻\(yùn)行結(jié)果正確性的影響,仍然可以算是一個(gè)相對(duì)簡(jiǎn)單可控的問(wèn)題。但如果是在多線程環(huán)境中,就會(huì)延伸出一些更嚴(yán)重的問(wèn)題。
我們考慮一個(gè)簡(jiǎn)單銀行賬戶系統(tǒng),并考慮一下并發(fā)存款或者取款的情形:
class account {
var balance: Double
init(balance: Double) {
self.balance = balance
}
func withdraw(amount: Double) {
let newBalance = self.balance - amount // #1
self.balance = newBalance // #2
}
func deposit(amount: Double) {
let newBalance = self.balance + amount
self.balance = newBalance
}
}
let george = account(balance: 100)
let paul = george
george.withdraw(10)
paul.withdraw(20)
這個(gè)例子中,可以認(rèn)為Paul和George共享了一個(gè)銀行賬戶。George和Paul在不同的地方同時(shí)取款。這種情況我們可以在兩個(gè)并發(fā)線程中分別執(zhí)行g(shù)eorge.withdraw(10)和paul.withdraw(20)來(lái)模擬。我們有可能會(huì)得到錯(cuò)誤的余額結(jié)果,這對(duì)銀行來(lái)說(shuō)可能不是好事。
如果出現(xiàn)以下執(zhí)行順序,情況就不太美妙:
- 首先,George執(zhí)行完了#1語(yǔ)句,得到了newBalance的值為90。
- 同時(shí),Paul在另外一個(gè)線程中也執(zhí)行完了#1語(yǔ)句,得到了newBalance的值為80。
- 然后,George執(zhí)行#2語(yǔ)句,用newBalance為90更新了self.balance,余額減為90元。
- 最后,Paul執(zhí)行#2語(yǔ)句,他悲劇地以值為80的newBalance更新了self.balance,余額最終被更新為80元。
這當(dāng)然是錯(cuò)誤的結(jié)果,余額最開(kāi)始為100元,George取了10元,Paul取了20元,余額應(yīng)該是70元。銀行因?yàn)檫@個(gè)并發(fā)錯(cuò)誤虧損了10元。仔細(xì)查看以上過(guò)程,可以發(fā)現(xiàn)錯(cuò)誤發(fā)生在Paul將余額更新為80元時(shí),其實(shí)存在一個(gè)前提:更新之前余額應(yīng)該是100元。但不幸的是在George將余額修改為90元之后,上述前提已再不合法。更不幸的是,在實(shí)際情況中,這類錯(cuò)誤并不是每次都會(huì)發(fā)生。這取決于各個(gè)線程以何種順序執(zhí)行代碼。而這種不能穩(wěn)定復(fù)現(xiàn)的錯(cuò)誤,常常難以修復(fù)。
這個(gè)錯(cuò)誤也揭示了,時(shí)間在程序中所產(chǎn)生的影響。計(jì)算結(jié)果需要依賴各個(gè)賦值發(fā)生的順序。并發(fā)情況下,正確地控制這種順序變得更加復(fù)雜了。
很多工具和并發(fā)控制策略被發(fā)明出來(lái)用于解決并發(fā)問(wèn)題:原子操作,阻塞,信號(hào),鎖。但這些工具和策略仍然很復(fù)雜,讓程序員掌握這些工具并不容易,有些還會(huì)影響程序的運(yùn)行效率。而且例如死鎖這樣的問(wèn)題,即使引入復(fù)雜的死鎖避免技術(shù),在一些地方也仍然無(wú)法完全避免。
引入賦值之前,程序沒(méi)有時(shí)間的問(wèn)題,變量任何時(shí)候具有某個(gè)值,將總是具有這個(gè)值。引入賦值之后,我們就必須開(kāi)始考慮時(shí)間在計(jì)算中的作用。在并發(fā)情況下,由賦值引入的復(fù)雜性變得更加嚴(yán)重了。需要在程序中考慮時(shí)間的作用的負(fù)擔(dān)變得越來(lái)越嚴(yán)重了。
時(shí)至今日,要編寫線程安全的,且性能可靠的并發(fā)環(huán)境下執(zhí)行的程序,對(duì)命令式編程語(yǔ)言來(lái)說(shuō),仍然是嚴(yán)峻的考驗(yàn)。這個(gè)問(wèn)題直接促使Swift,Scala這樣的新興語(yǔ)言開(kāi)始從函數(shù)式編程語(yǔ)言中尋找靈感,來(lái)解決或者緩解并發(fā)問(wèn)題。
總結(jié)
Swift中有兩個(gè)聲明變量的關(guān)鍵字:let和var。這兩個(gè)關(guān)鍵字背后存在著兩種截然不同的編程思想:函數(shù)式編程和命令式編程。 Swift對(duì)這兩種編程思想進(jìn)行了融合:它允許你使用引入賦值所帶來(lái)的簡(jiǎn)單直觀的建模方法。同時(shí)也鼓勵(lì)你使用不變性緩解各類并發(fā)問(wèn)題。