簡介
Swift中有兩種聲明“變量”的方式,這兩種方式分別使用let和var這兩個關(guān)鍵字次企。這應(yīng)該是借鑒了Scala怯晕,因為它們和Scala的val和var有相同的作用。let被用于聲明不變量缸棵,var被用于聲明變量舟茶。不變量的值一旦被定義就不能再改變,變量則可以在聲明之后被隨意賦值。
在其它一些如Java吧凉,C這樣的命令式編程語言中也有不變量的概念隧出。但多數(shù)情況下會被以常量形式使用,常量是靜態(tài)的不變量阀捅。在Java中胀瞪,通常用static和final一起來定義常量,其中static用于指明其是靜態(tài)的饲鄙,final用于指明其是不變的凄诞。Java中,我們有多種定義常量的方法:接口中定義忍级,類中定義帆谍,使用枚舉實現(xiàn)。這些方法之間的區(qū)別是在何時何地如何使用static和final轴咱。Objective-C汛蝙,則和C語言一樣,使用const關(guān)鍵字說明一個變量不應(yīng)被改變朴肺。
在這類語言中窖剑,不變量和變量相比,通常是不尋常的戈稿,次一等的概念西土。如果將一個名字關(guān)聯(lián)到一個值,缺省的會得到一個變量鞍盗,而不是不變量翠储。如果,你需要一個不會改變橡疼,一直和某個特定值綁定的名字,就需要顯式說明它是不變的庐舟。例如欣除,在Java中使用final,在C中使用const挪略。這種缺省就是變量的情況历帚,甚至影響了我們的語言。當(dāng)我們需要描述杠娱,“聲明用于和某個值關(guān)聯(lián)的名字”時挽牢,我們說的是“聲明變量”。但其實摊求,這個“變量”應(yīng)該加上引號禽拔,因為它其實可能是個不變量。這和指代不明確性別人時,使用“他”而不是“她”是同一類現(xiàn)象睹栖。
“缺省的是變量硫惕,如果需要不變量,請顯式說明”野来。這是大多數(shù)命令式編程語言對變量和不變量的處理方法恼除。這很自然。因為這類語言的設(shè)計中曼氛,大多數(shù)情況下使用的是變量豁辉,不變量只是在特殊情況下才需要。Swift(和Scala一樣)則對這種設(shè)計做出了修改舀患。從缺省是變量徽级,轉(zhuǎn)變?yōu)檎J(rèn)為變量和不變量的地位是平等的。不變量應(yīng)該更多被提倡和使用构舟。在Swift的語法中灰追,對這種設(shè)計思想的體現(xiàn)是:在定義一個和值關(guān)聯(lián)的名字時,需要明確地使用var或let說明它是變量還是不變量狗超。
Swift弹澎,和Java,C努咐,Objective-C等語言相比苦蒿,為何會有這種對待不變量的觀點(diǎn)的變化呢?
變量和不變量其實源于兩種不同編程范式渗稍。編程范式是編程語言設(shè)計者所持有的“世界觀”的反映佩迟。
變量來源于命令式編程范式。這種編程范式將世界視為一系列獨(dú)立的對象的組合竿屹,這些對象的行為可能會隨著時間變化而不斷變化报强。程序語言中的變量被用于模擬對象的狀態(tài)。
不變量來源于函數(shù)式編程范式拱燃。這種編程以數(shù)學(xué)函數(shù)為建模核心秉溉。試圖將世界抽象成為以一系列數(shù)學(xué)函數(shù)。數(shù)學(xué)函數(shù)中的變量其實和命令式編程語言中的變量存在著顯著的區(qū)別碗誉≌偎唬基于數(shù)學(xué)的函數(shù)式編程中的變量的概念更接近于命令式編程中的不變量。這在后續(xù)章節(jié)會詳細(xì)討論哮缺。
我們甚至可以通過對變量的態(tài)度來定義命令式編程和函數(shù)式編程:廣泛采用賦值的程序設(shè)計被稱為命令式程序設(shè)計弄跌;不使用任何被賦值的程序設(shè)計被稱為函數(shù)式程序設(shè)計。這是因為尝苇,賦值操作使得變量可變铛只。沒有賦值操作埠胖,則變量不可變。
Swift受到了函數(shù)式編程的影響格仲,強(qiáng)化了不變量在語言中位置押袍,鼓勵不變量的使用。
函數(shù)式編程中的變量
函數(shù)式編程以數(shù)學(xué)函數(shù)為建目撸基礎(chǔ)谊惭。其變量的概念和數(shù)學(xué)中變量的概念是一致的。所以侮东,我們可以先回顧一下數(shù)學(xué)函數(shù)中變量的概念圈盔。由于現(xiàn)在絕大多數(shù)程序設(shè)計語言是命令式的,所以我們通常所說的變量是命令式編程中的定義悄雅,這和數(shù)學(xué)函數(shù)中的變量并不相同驱敲。
在數(shù)學(xué)中,函數(shù)是描述每個輸入值對應(yīng)唯一輸出值的這種對應(yīng)關(guān)系宽闲。數(shù)學(xué)函數(shù)中的變量是一個用于表示值的符號众眨,值是可以是隨意的,也可能是未定的容诬。所以娩梨,在數(shù)學(xué)函數(shù)中,某個符號我們之所以稱其為變量览徒,是因為它可以用于代表不同的值狈定。而需要指明的是:當(dāng)我們用明確的數(shù)值代入函數(shù)運(yùn)算時,變量就擁有了明確的值习蓬。而在一次代換過程中纽什,變量一旦被代換為明確的值,就不會再次改變?yōu)槠渌刀愕稹?shù)學(xué)函數(shù)中不存在這種情況:某一次代換過程中芦缰,某個變量x一開始被代換為2,然后又變?yōu)?枫慷。這在數(shù)學(xué)上饺藤,沒有任何意義。
這樣看起來流礁,數(shù)學(xué)函數(shù)中的變量其實應(yīng)該可以對應(yīng)程序語言中的不變量:一旦被定義,就不再變化罗丰。純粹的函數(shù)式編程語言就完整繼承了這種數(shù)學(xué)上的變量概念神帅。例如,Haskell就沒有可變量的概念萌抵,聲明一個變量找御,只能被賦值一次元镀,之后就不會再變化。而命令式編程語言中霎桅,變量被定義之后栖疑,仍然能夠隨意被賦予其它的值。
比如我們有一個簡單的數(shù)學(xué)函數(shù):
f(x) = 2*x + x * x
如果滔驶,我們遵循數(shù)學(xué)函數(shù)對變量的看法遇革,可以將其翻譯為如下的Swift函數(shù)。這個程序函數(shù)和上面的數(shù)學(xué)函數(shù)揭糕,在概念上是等價的萝快。
func foo(x: Int) -> Int {
return 2*x + x * x
}
當(dāng)然,這個Swift函數(shù)foo著角,還有其它現(xiàn)實方法揪漩。函數(shù)的另外一種實現(xiàn)bar為了展示y是一個命令式編程里的變量,而稍顯怪異吏口。但它仍然能得到和上面的函數(shù)相同的答案:代入任意相同的x值奄容,兩個函數(shù)都會得到相同的返回值。但由于數(shù)學(xué)函數(shù)中不存在y這樣的一開始等于某個值产徊,而后又被賦為另一個值這樣的命令式編程中的變量概念昂勒。所以,我們沒有辦法將下面這樣的Swift函數(shù)bar還原為一個概念上一致的數(shù)學(xué)函數(shù)囚痴。
func bar(x: Int) -> Int {
var y = 2 * x
y = y + x * x
return y
}
Swift中提供let聲明不變量叁怪,更為重視不變性,明確鼓勵在更多的場合使用不變量深滚。這都是受函數(shù)式編程中變量的不變性的影響奕谭。后面會討論Swift為何會受到這種影響。
命令式編程中的變量
命令式編程語言中的變量的概念為大多數(shù)程序員所熟悉痴荐。我們將其和函數(shù)式編程中的變量做一個對比:在函數(shù)式編程中血柳,變量其實并不可變,這種變量只是一個代表了某個值的符號生兆。而在命令式編程中难捌,由于變量是可變的,變量就不僅僅是簡單代表一個值的符號鸦难,而是索引了一個可以保存值的位置根吁,在這個位置上可以存放不同的值。
我們的世界中每個對象都有著自己隨著時間變化的狀態(tài)合蔽。而在不同時刻击敌,變量可以代表了不同的值,使得變量擁有了時序上的概念拴事。我們就可以使用變量來模擬和刻畫現(xiàn)實世界中的對象的狀態(tài)沃斤。這其實也是為何會引入賦值圣蝎,使得變量可變的原因。
引入賦值的好處
如果使用過一些函數(shù)式編程語言衡瓶,就會發(fā)現(xiàn)部分函數(shù)式編程語言并沒有完全拋棄賦值徘公。在Scheme中,我們?nèi)匀豢梢杂?set! x 15)這樣的語句為變量賦值哮针,變量將在賦值前后和不同的值關(guān)聯(lián)关面。為何這些函數(shù)式編程語言沒有完整地貫徹變量的不變性呢?
函數(shù)式編程語言出現(xiàn)的時間很早诚撵,最早的函數(shù)式編程語言Lisp是歷史第二悠久的高級編程語言(僅次于Fortran)缭裆。但現(xiàn)在函數(shù)式編程語言并沒有成為絕大多數(shù)程序員的工作語言。為何現(xiàn)今流行的編程語言:C寿烟,Java澈驼,C++,Python都是命令式編程語言呢筛武?
這是因為缝其,引入賦值,使得變量可變徘六。就引入了一個簡單直觀又易于模塊化的程序語言建模方法内边。這在設(shè)計大型軟件系統(tǒng)時是一個巨大的優(yōu)勢。
命令式編程的建模思想是一種直觀的世界觀:“世界是由聚集在一起的一系列獨(dú)立的對象組成的”待锈。但這僅僅是在一個維度上的描述漠其。另外一個時間維度上的描述通常不被提及:“每個對象都有著隨時間變化的狀態(tài)”。綜合來說就是:“世界由對象組成竿音,對象都有狀態(tài)”和屎。將這種直觀的世界觀引入程序設(shè)計所帶來的好處是,建模更為簡單了春瞬。使用這種思想的編程語言對于程序員來說也更為簡單直觀了柴信。那么將一個實際問題用這種編程語言中的概念來描述,也就變得更輕松了宽气。因為随常,程序員通常能夠為實際問題中的事物一一對應(yīng)地構(gòu)建對象,并按時序描述每個對象的狀態(tài)萄涯。
如果將賦值和局部變量結(jié)合绪氛,構(gòu)造帶局部狀態(tài)的對象,就可以提供一種有利于系統(tǒng)模塊化設(shè)計的技術(shù)涝影。這是一種強(qiáng)大的設(shè)計策略钞楼,原因在于它的簡單和直觀。我們可以直接構(gòu)造那些用于模擬真實物理系統(tǒng)的對象袄琳。對于問題域里的每個對象询件,我們都可以構(gòu)造一個與之相對應(yīng)的計算機(jī)程序里的對象。如果唆樊,我們能把對象的“狀態(tài)”局限在對象內(nèi)部宛琅,使之成為“局部狀態(tài)”(這其實就是封裝)。然后逗旁,將各自具有“局部狀態(tài)”的對象組合嘿辟,這會是一個良好的模擬真實世界的手段。
我們之所以可以使用UML(Unified Modeling Language)來分析項目需求片效,是因為我們將在項目中使用命令式編程語言红伦。從UML這種圖形化的輔助建模方式中,我們可以更明顯地看到如何將真實世界中的對象和程序語言中的對象一一對應(yīng)淀衣,如何將真實世界中的對象的一個個屬性和程序語言中的對象的變量一一對應(yīng)昙读。
如果,使用函數(shù)式編程語言膨桥,UML將不再能起到任何作用蛮浑。你需要的是一個類似將現(xiàn)實問題抽象為數(shù)學(xué)問題的過程。這種數(shù)學(xué)的建模方式對大多數(shù)人來說可能都會更為困難一些只嚣。
引入賦值的代價
在函數(shù)式編程中引入賦值沮稚,存在著一些爭議。仍然有如Haskell這樣的函數(shù)式編程語言册舞,堅持純粹的函數(shù)式編程思想蕴掏,不使用任何賦值操作(當(dāng)然,仍然有使用不變量難以描述的情況存在调鲸。Haskell社區(qū)稱這部分為有副作用的盛杰,不純的。這部分代碼會被限制在Monad中實現(xiàn))线得。
也有Swift和Scala這樣的新興語言饶唤,重新思考函數(shù)式編程語言中不變性的意義。在語言設(shè)計中贯钩,強(qiáng)調(diào)和重視不變性募狂。
這是因為沒有免費(fèi)的午餐。引入賦值角雷,除了上節(jié)所說的帶來了一個簡單直觀又易于模塊化的程序語言建模方法之外祸穷,也引入了一些缺陷,我們需要為此付出一些代價勺三。其中一些缺陷使得我們在構(gòu)建大規(guī)模軟件系統(tǒng)時雷滚,遇到了一些難以克服的困難。
更復(fù)雜的計算模型
為函數(shù)式編程語言引入賦值語句吗坚,使得變量可變祈远〈敉颍看起來只是多了賦值語法,但其實這并不是一件簡單的事情车份。賦值的引入對編程語言造成的影響是巨大的:隨著賦值的引入谋减,我們必須為編程語言引入一種更為復(fù)雜的計算模型。
在沒有賦值語句之前扫沼,純函數(shù)式編程語言可以使用數(shù)學(xué)上的代換模型來構(gòu)建語言的計算模型:一個變量可以安全地被代換為它所代表的表達(dá)式或者值出爹。求值一個純函數(shù)式編程語言中的函數(shù),和求值一個數(shù)學(xué)函數(shù)并沒有什么區(qū)別缎除。你可以認(rèn)為編程語言的運(yùn)行方式和數(shù)學(xué)的運(yùn)算方式是一樣的严就。這種代換模型其實是一個相當(dāng)簡單的語言模型。
但在引入賦值之后器罐,變量在程序運(yùn)行的某些時刻代表一個值梢为,在另一些時刻代表另外一個值。代換模型就不再有效了技矮。因為抖誉,代換模型基于數(shù)學(xué)模型。數(shù)學(xué)上并沒有在某些時刻代表一個值衰倦,在另一些時刻代表另外一個值的變量概念袒炉。如果嘗試對帶有賦值操作的函數(shù)進(jìn)行代換,會發(fā)現(xiàn)當(dāng)遇到賦值語句時樊零,代換過程無法進(jìn)行下去我磁。因為變量已經(jīng)不能被再被看做是某個值的名字了。此時的變量以某種方式指定了一個“位置”驻襟,我們可以將任何值存儲在該“位置”夺艰。那到底是將哪個值代入變量呢?在代換模型中沉衣,無法解決該問題郁副。
為了解決這個問題滋尉,我們引入更為復(fù)雜的環(huán)境模型律歼。變量將維持在我們稱為“環(huán)境”的結(jié)構(gòu)中欠痴。環(huán)境包含一系列約束屿笼,這些約束將一些變量的名字關(guān)聯(lián)到對應(yīng)值。在環(huán)境模型中冈敛,變量的值將取決于其所處的環(huán)境遭铺。程序運(yùn)行過程中搀绣,環(huán)境時常變化栋艳,變量的值也就隨之改變恰聘。
引入更復(fù)雜的計算模型意味著實現(xiàn)編程語言變得更為困難了。
同一問題的復(fù)雜化
相等的判斷
我們拋開具體的程序語言討論一下如何判斷對象相等。在程序語言中晴叨,有一種從效果上判斷相同的方法:如果在任意計算中用一個對象替換另外一個對象凿宾,都不會改變結(jié)果,那么我們就可以認(rèn)為這兩個對象相等兼蕊。
如果菌湃,沒有賦值操作存在。我們判斷對象相等會簡單一些遍略。例如,在下面例子中骤坐,let使Point的實例變量x和y都成為不變量绪杏。p1和p2的x,y相等纽绍,而且兩個點(diǎn)的x,y值都不會改變蕾久。所以,可以認(rèn)為在任何時候的任何計算中拌夏,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來聲明Point的實例變量盹愚。下面例子中的p1和p2相等的結(jié)論就不一定正確了。因為站故,我們可以使用賦值操作來改變點(diǎn)的實際坐標(biāo)了皆怕。當(dāng)執(zhí)行p1.x = 2之后,顯然它們就無法在任何計算中相互替換了西篓。我們不能認(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)
可以看到在引入賦值之后,判斷兩個對象是否相等的問題變得更為復(fù)雜了岂津。
別名
在擁有賦值操作后虱黄,另外一個經(jīng)常引起困惑和錯誤的是別名問題。一個對象可以通過多個名字訪問的的現(xià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是兩個獨(dú)立對象,p3是p1的別名赁豆。這兩組關(guān)系之間有微妙的區(qū)別仅醇,我們常常在實際編程過程中混淆兩者。p1和p2對各自的修改互不影響魔种,可以認(rèn)為它們是兩個獨(dú)立的點(diǎn)析二。而p1和p3可以認(rèn)為是一個點(diǎn)。對其中任何一個的修改都會造成另一個也同樣被修改。如果叶摄,我們想在程序中搜索出p1可能被修改的地方属韧,就必須記住,也要檢查那些修改了p3的地方蛤吓。然而在實際編程中宵喂,特別是在大型復(fù)雜系統(tǒng)中,我們常常會忘記会傲,或者根本就不知道p3是某個對象(這里是p1)的別名锅棕。要么,修改了p3淌山,卻不知道也造成了p1的修改裸燎。這種副作用常常防不勝防,在編程中經(jīng)常出現(xiàn)泼疑。要么德绿,在需要對修改操作做重新設(shè)計時,只顧及了p3退渗,而忘記同時也要修改p1的地方移稳。這種別名常常難以被識別而被遺忘。
但是会油,如果沒有賦值操作个粱,別名造成的困擾就消失了。即使在實際物理內(nèi)存上钞啸,這兩組關(guān)系并不相同:p1和p2指向兩塊不同的內(nèi)存地址几蜻,p1和p3指向同一塊內(nèi)存地址。但你仍然可以認(rèn)為p1体斩,p2梭稚,p3是相等的對象。因為絮吵,在沒有賦值的情況下弧烤,它們在任何計算中都可以相互替換。是否是別名在計算中并沒有什么區(qū)別蹬敲。
值類型和引用類型
也許有人發(fā)現(xiàn):開始暇昂,我們使用結(jié)構(gòu)體(struct)實現(xiàn)Point,而后在解釋別名問題時又改用類(class)實現(xiàn)Point伴嗡。這是因為Swift擴(kuò)大了值類型的使用范圍急波。
在Java中,可以認(rèn)為原始類型(int瘪校,long澄暮,float名段,double,short泣懊,char伸辟,boolean)是值類型,而其它繼承自O(shè)bject的類型都是引用類型馍刮。
而在Swift中信夫,結(jié)構(gòu)體被設(shè)計成一種值類型。整數(shù)卡啰,浮點(diǎn)數(shù)静稻,布爾值,字符串匈辱,數(shù)組和字典在Swift中都是以結(jié)構(gòu)體的形式實現(xiàn)的姊扔,所以,它們也都是值類型梅誓。特別是數(shù)組,字典這種常用集合類型也被實現(xiàn)為值類型佛南,使得值類型在Swift中的使用范圍大大擴(kuò)展了梗掰。
值類型在被賦給一個變量,或者被傳遞給函數(shù)時嗅回,實際上是做了一次拷貝及穗。與值類型對應(yīng)是引用類型。引用類型在被賦給一個變量绵载,或者被傳遞給函數(shù)時埂陆,是傳遞的是引用。類(class)仍然是引用類型娃豹。所以焚虱,類實現(xiàn)的Point會有別名的問題。而值類型不會有這類別名所帶來的問題懂版。
在下面用結(jié)構(gòu)體實現(xiàn)Point的例子中鹃栽,p3不再是p1的別名,而是p1的一個拷貝躯畴。
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
我們可能會問一個問題:如果每次賦值都進(jìn)行拷貝民鼓,是否會大大增加內(nèi)存開銷呢?如果每次賦值都進(jìn)行對象拷貝蓬抄,確實會增大內(nèi)存開銷丰嘉。Swift的解決方案是:只在值類型發(fā)生改變時才進(jìn)行拷貝。就上面的結(jié)構(gòu)體實現(xiàn)的Point的例子而言嚷缭,var p3 = p1雖然進(jìn)行了賦值饮亏,但這時還并沒有發(fā)生拷貝操作。這時,p3其實仍然是p1的別名克滴,它們指向同一個內(nèi)存地址逼争。直到我們改變p3了,比如執(zhí)行p3.x = 2時劝赔,才會先發(fā)生拷貝誓焦,然后在拷貝的副本上進(jìn)行賦值修改操作。這么做當(dāng)然節(jié)省了內(nèi)存開銷着帽。而可以這么做的根據(jù)是:沒有賦值操作時杂伟,同一問題更簡單了,別名并不會帶來問題仍翰。在這種沒有賦值的情況下赫粥,值類型和引用類型其實可以被認(rèn)為是等效的。
擴(kuò)大值類型的使用范圍是Swift減緩別名問題的一種方式予借。另外一種方式越平,則是我們在本文中一直討論的:由于賦值操作的引入,使得同一問題復(fù)雜化了灵迫。那么秦叛,即使現(xiàn)在做不到完全去除賦值操作,一定程度上鼓勵不變性瀑粥,在需要的環(huán)境中使用不變量挣跋,也能緩解這種復(fù)雜性所帶來的問題。
賦值順序
可以舉一個求階乘的例子來說明狞换,賦值語句的相對順序?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
}
這個例子中,如果我們將product = i * product和i = i + 1兩條語句的執(zhí)行順序互換修噪,將會得到不同的結(jié)果查库。一般而言,帶有賦值的程序?qū)?qiáng)迫程序員考慮賦值的相對順序黄琼,以保證每個語句所用的是被修改變量的正確版本膨报。這增加了程序員的負(fù)擔(dān)。使得程序員每次用到賦值時适荣,都需要清楚變量的賦值操作之間的相對順序现柠。
函數(shù)式編程語言中,由于沒有賦值弛矛,所以根本沒有這類問題够吩。為了對比,下面例子使用函數(shù)式編程的風(fēng)格再次實現(xiàn)階乘丈氓。在函數(shù)式編程中周循,一般會使用遞歸來代替命令式編程中所用到的循環(huán)結(jié)構(gòu)强法。這樣風(fēng)格的代碼中,我們無法體會到對于時序的要求湾笛。
func factorial(n: Int) -> Int {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
并發(fā)問題
在單線程環(huán)境中饮怯,考慮賦值操作的相對順序?qū)Τ绦蜻\(yùn)行結(jié)果正確性的影響,仍然可以算是一個相對簡單可控的問題嚎研。但如果是在多線程環(huán)境中蓖墅,就會延伸出一些更嚴(yá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)
這個例子中论矾,可以認(rèn)為Paul和George共享了一個銀行賬戶。George和Paul在不同的地方同時取款杆勇。這種情況我們可以在兩個并發(fā)線程中分別執(zhí)行g(shù)eorge.withdraw(10)和paul.withdraw(20)來模擬贪壳。我們有可能會得到錯誤的余額結(jié)果,這對銀行來說可能不是好事蚜退。
如果出現(xiàn)以下執(zhí)行順序闰靴,情況就不太美妙:
- 首先,George執(zhí)行完了#1語句钻注,得到了newBalance的值為90传黄。
- 同時,Paul在另外一個線程中也執(zhí)行完了#1語句队寇,得到了newBalance的值為80。
- 然后章姓,George執(zhí)行#2語句佳遣,用newBalance為90更新了self.balance,余額減為90元凡伊。
- 最后零渐,Paul執(zhí)行#2語句,他悲劇地以值為80的newBalance更新了self.balance系忙,余額最終被更新為80元诵盼。
這當(dāng)然是錯誤的結(jié)果,余額最開始為100元银还,George取了10元风宁,Paul取了20元,余額應(yīng)該是70元蛹疯。銀行因為這個并發(fā)錯誤虧損了10元戒财。仔細(xì)查看以上過程,可以發(fā)現(xiàn)錯誤發(fā)生在Paul將余額更新為80元時捺弦,其實存在一個前提:更新之前余額應(yīng)該是100元饮寞。但不幸的是在George將余額修改為90元之后孝扛,上述前提已再不合法。更不幸的是幽崩,在實際情況中苦始,這類錯誤并不是每次都會發(fā)生。這取決于各個線程以何種順序執(zhí)行代碼慌申。而這種不能穩(wěn)定復(fù)現(xiàn)的錯誤陌选,常常難以修復(fù)。
這個錯誤也揭示了太示,時間在程序中所產(chǎn)生的影響柠贤。計算結(jié)果需要依賴各個賦值發(fā)生的順序。并發(fā)情況下类缤,正確地控制這種順序變得更加復(fù)雜了臼勉。
很多工具和并發(fā)控制策略被發(fā)明出來用于解決并發(fā)問題:原子操作,阻塞餐弱,信號宴霸,鎖。但這些工具和策略仍然很復(fù)雜膏蚓,讓程序員掌握這些工具并不容易瓢谢,有些還會影響程序的運(yùn)行效率。而且例如死鎖這樣的問題驮瞧,即使引入復(fù)雜的死鎖避免技術(shù)氓扛,在一些地方也仍然無法完全避免。
引入賦值之前论笔,程序沒有時間的問題采郎,變量任何時候具有某個值,將總是具有這個值狂魔。引入賦值之后蒜埋,我們就必須開始考慮時間在計算中的作用。在并發(fā)情況下最楷,由賦值引入的復(fù)雜性變得更加嚴(yán)重了整份。需要在程序中考慮時間的作用的負(fù)擔(dān)變得越來越嚴(yán)重了。
時至今日籽孙,要編寫線程安全的烈评,且性能可靠的并發(fā)環(huán)境下執(zhí)行的程序,對命令式編程語言來說犯建,仍然是嚴(yán)峻的考驗础倍。這個問題直接促使Swift,Scala這樣的新興語言開始從函數(shù)式編程語言中尋找靈感胎挎,來解決或者緩解并發(fā)問題沟启。
總結(jié)
Swift中有兩個聲明變量的關(guān)鍵字:let和var忆家。這兩個關(guān)鍵字背后存在著兩種截然不同的編程思想:函數(shù)式編程和命令式編程。 Swift對這兩種編程思想進(jìn)行了融合:它允許你使用引入賦值所帶來的簡單直觀的建模方法德迹。同時也鼓勵你使用不變性緩解各類并發(fā)問題芽卿。