Swift支持函數(shù)式編程,這一篇介紹不變性(immutable)。
不變性是函數(shù)式編程的基礎(chǔ)。
先討論一下Haskell這類純函數(shù)式語言。簡(jiǎn)單而言,Haskell沒有變量。這是因?yàn)?,Haskell追求更高級(jí)別的抽象,而變量其實(shí)是對(duì)一類低級(jí)計(jì)算機(jī)硬件:存儲(chǔ)器空間(寄存器,內(nèi)存)的抽象。變量存在的原因,可以視為計(jì)算機(jī)語言進(jìn)化的遺跡,比如在初期直接操作硬件的匯編語言中,需要變量來使用操作存儲(chǔ)過程。而在計(jì)算機(jī)出現(xiàn)之前,解決數(shù)學(xué)計(jì)算問題都是圍繞構(gòu)建數(shù)學(xué)函數(shù)。數(shù)學(xué)中,不存在計(jì)算機(jī)語言中這種需要重復(fù)賦值的變量。
而Haskell則基于更抽象的數(shù)學(xué)模型。使用Haskell編程只需專注于設(shè)計(jì)數(shù)據(jù)之間的映射關(guān)系。而在數(shù)學(xué)上,表示兩個(gè)數(shù)據(jù)之間映射關(guān)系的實(shí)體就是函數(shù)。這使得編寫Haskell代碼和設(shè)計(jì)數(shù)學(xué)函數(shù)的過程是一致的,Haskell程序員的思路也更接近數(shù)學(xué)的本質(zhì)。
Haskell摒棄了變量的同時(shí),也拋棄了循環(huán)控制。這是因?yàn)闆]有變量,也就沒有了控制循環(huán)位置的循環(huán)變量。這也很好理解?;貞浺幌挛覀?cè)趯W(xué)習(xí)計(jì)算機(jī)之前的數(shù)學(xué)課程中,也無需使用到for這類概念。我們還是使用函數(shù)處理一個(gè)序列到另外一個(gè)序列的轉(zhuǎn)換。
Swift提供了一定程度的不變性。在Swift中,被聲明為不變的對(duì)象在完成對(duì)其初始構(gòu)造之后就不可改變。換句話說,構(gòu)造器是唯一個(gè)可以改變對(duì)象狀態(tài)的地方。如果你想改變一個(gè)對(duì)象的值,只能使用修改后的值來創(chuàng)建新的對(duì)象。
不變性是為了減少或者消滅狀態(tài)。面向?qū)ο缶幊陶Z言中,狀態(tài)是計(jì)算的基礎(chǔ)信息。如何可控地修改狀態(tài),Java,Ruby等編程語言都給出了大量的語言機(jī)制,比如,可見性分級(jí)。但是,由于大量可變狀態(tài)的存在,使用面向?qū)ο缶幊陶Z言在編寫高并發(fā),多線程代碼時(shí)會(huì)有很多困難。因?yàn)椋銦o法知道并行進(jìn)行的諸多狀態(tài)讀寫中是否有順序上的錯(cuò)誤。而且這種錯(cuò)誤又是難以察覺的。而不變性解決了這個(gè)問題。不變性意味函數(shù)沒有副作用,無論多少次執(zhí)行,相同的輸入就意味著相同的輸出。那么,多線程環(huán)境中就沒有了煩人的同步機(jī)制。所有線程都可以無所顧忌的執(zhí)行同一個(gè)函數(shù)的代碼。
而在Java這類面向?qū)ο缶幊陶Z言中,變量用于表示對(duì)象本身的狀態(tài)。Swift作為支持多種范型的編程語言,即支持變量,也支持方便地申明不變量。
Java中,聲明不變量:
#變量
private string mutable;
#不變量
private final String immutable;
Scala中,聲明不變量:
#變量
var mutable
#不變量
val immutable = 1
Swift中聲明變量和不變量:
#變量
var mutable
#不變量
let immutable = 1
Swift中聲明了不變量,就必須在聲明時(shí)同時(shí)初始化,或者在構(gòu)造器中初始化。這兩個(gè)地方之外,就無法再改變不變量了。Swift區(qū)分var
和let
不僅僅是為了區(qū)分變量和不變量,同時(shí)也是為了使用編譯器來強(qiáng)制這種區(qū)分。聲明不變量是受到鼓勵(lì)的。因?yàn)?,使用不變量更容易寫出,容易理解,容易測(cè)試,松耦合的代碼。
由于不可變性具有例如線程安全性這類天生優(yōu)勢(shì),在編寫面向?qū)ο笳Z言時(shí),我們也會(huì)有使用到不變對(duì)象的場(chǎng)景。但由于編程范式不同的原因,在面向?qū)ο笳Z言中構(gòu)造不可變類是一件非常麻煩的事情。
以Java為例,如果將一個(gè)類構(gòu)造成不可變的類,需要做如下事情:
將類聲明為final。這樣就不能繼承該類。無法繼承該類,就無法重寫它的方法的行為。Java 中的String 類就使用了這種策略。
所有的實(shí)例變量都聲明為final。這樣,你就必須在申明時(shí)初始化它,或者在構(gòu)造器中初始化它們。在其他地方,你都將無法改變聲明為final的實(shí)例變量。
提供合適的構(gòu)造過程。對(duì)于不可變類,構(gòu)造器是唯一可以初始化它的地方。所以,提供一個(gè)合適的構(gòu)造器是實(shí)用不可變類的必要條件。
一個(gè)Java實(shí)現(xiàn)的不可變類的例子如下:
public final class Person {
private final String name;
private final List<String> interests;
public Person(String name, List<String> interests) {
this.name = name;
this.streets = streets;
this.city = city;
}
public String getName() {
return name;
}
public List<String> getInterests() {
return Collections.unmodifiableList(interests);
}
}
具有函數(shù)特性的多范式編程語言中,大多數(shù)會(huì)為構(gòu)造不變類提供方便。比如Groovy提供了@Immutable
注釋來表示不可變類。
@Immutable
class Preson {
String name
String[] interests
}
@Immutable
提供了以下功能:
Swift實(shí)現(xiàn)一個(gè)不可變類的方法的例子:
struct Person {
let name:String
let interests:[String]
}
let
聲明的實(shí)例變量,保證了類初始化之后,實(shí)例變量無法再被改變;Swift中實(shí)現(xiàn)一個(gè)不可變的類的方法是:聲明一個(gè)結(jié)構(gòu)體(struct
),并將該結(jié)構(gòu)體的所有實(shí)例變量以let
開頭聲明為不變量。在不變性這方面,枚舉(enum
)具有和結(jié)構(gòu)體相同的特性。所以,上面例子中的結(jié)構(gòu)體在合適的場(chǎng)景下,也可以被枚舉類型替換。
?值類型在賦值和作為函數(shù)參數(shù)的時(shí)候被傳遞給一個(gè)函數(shù)的時(shí)候,實(shí)際上操作的是其的拷貝。Swift中有大量值類型,包括數(shù)字,字符串,數(shù)組,字典,元組,枚舉和結(jié)構(gòu)體等。
struct PersonStruct {
var name:String
}
var structPerson = PersonStruct(name:"Totty")
var sameStructPerson = structPerson
sameStructPerson.name = "John"
print(structPerson.name)
print(sameStructPerson.name)
// result:
// "Totty"
// "John"
可以看到,structPerson和sameStructPerson的值不一樣了。在賦值的時(shí)候,sameStructPerson的到是structPerson的拷貝。
引用類的實(shí)例 (主要是類) 可以有多個(gè)所有者。在賦值和作為函數(shù)參數(shù)的時(shí)候被傳遞給一個(gè)函數(shù)的時(shí)候,操作的是其引用,而并不是其拷貝。這些引用都指向同一個(gè)實(shí)例。對(duì)這些引用的操作,都將影響同一個(gè)實(shí)例。
class PersonClass {
var name:String
}
var classPerson = PersonClass(name:"Totty")
var sameClassPerson = structPerson
sameClassPerson.name = "John"
print(classPerson.name)
print(sameClassPerson.name)
// result:
// "John"
// "John"
可以看到,sameClassPerson的改變,同樣也影響到了classPerson。其實(shí)它們指向同一個(gè)實(shí)例。這種區(qū)別在作為函數(shù)參數(shù)時(shí)也是存在的。
在Swift中區(qū)分值類型和引用類型是為了讓你將可變的對(duì)象和不可變的數(shù)據(jù)區(qū)分開來。Swift增強(qiáng)了對(duì)值類型的支持,鼓勵(lì)我們使用值類型。使用值類型,函數(shù)可以自由拷貝,改變值,而不用擔(dān)心產(chǎn)生副作用。
不變性導(dǎo)致另外一個(gè)結(jié)果,就是純函數(shù)。純函數(shù)即沒有副作用的函數(shù),無論多少次執(zhí)行,相同的輸入就意味著相同的輸出。一個(gè)純函數(shù)的行為并不取決于全局變量、數(shù)據(jù)庫的內(nèi)容或者網(wǎng)絡(luò)連接狀態(tài)。純代碼天然就是模塊化的:每個(gè)函數(shù)都是自包容的,并且都帶有定義良好的接口。純函數(shù)具有非常好的特性。它意味著理解起來更簡(jiǎn)單,更容易組合,測(cè)試起來更方便,線程安全性。
Objective-C中,蘋果的Foundation庫提供了不少具有不變性的類:NString相對(duì)于NSMutableString,NSArray相對(duì)于NSMutableArray,以及NSURL等等。在Objective-C中,絕大多數(shù)情況下,使用不變類是缺省選擇。但是,Objective-C中沒有如Swift中let
這樣簡(jiǎn)單強(qiáng)制不變性的方法。
不變性的好處:
更高層次的抽象。程序員可以以更接近數(shù)學(xué)的方式思考問題。
更容易理解的代碼。由于不存在副作用,無論多少次執(zhí)行,相同的輸入就意味著相同的輸出。純函數(shù)比有可變狀態(tài)的函數(shù)和對(duì)象理解起來要容易簡(jiǎn)單得多。你無需再擔(dān)心對(duì)象的某個(gè)狀態(tài)的改變,會(huì)對(duì)它的某個(gè)行為(函數(shù))產(chǎn)生影響。
更容易測(cè)試的代碼。更容易理解的代碼,也就意味著測(cè)試會(huì)更簡(jiǎn)單。測(cè)試的存在是為了檢查代碼中成功發(fā)生的轉(zhuǎn)變。換句話說,測(cè)試的真正目的是驗(yàn)證改變,改變?cè)蕉啵托枰蕉嗟臏y(cè)試來確保您的做法是正確的。如果你能有效的限制變化,那么錯(cuò)誤的發(fā)生的可能就更小,需要測(cè)試的地方也就更少。變化只會(huì)發(fā)生構(gòu)造器中,因此為不可變類編寫單元測(cè)試就成了一件簡(jiǎn)單而愉快的事情。
不像Haskell這種純函數(shù)式編程語言只能申明不可變量,Swift提供變量和不可變量?jī)煞N申明方式。這使得程序員有選擇的余地:在使用面向?qū)ο缶幊谭妒綍r(shí),可以使用變量。在需要的情況下,Swift也提供不變性的支持。
原文出處:http://lincode.github.io/Swift-Immutable
作者:LinGuo
更多建議: