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