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