在寫任何東西之前我需要承認(rèn)我是帶有偏見的:我愛 Swift。我認(rèn)為這是從我開始接觸 Cocoa 生態(tài)系統(tǒng)以來這個平臺上發(fā)生的最好的事情。我想通過分享我在 Swift,Objective-C 和 Haskell 上的經(jīng)驗(yàn)讓大家知道我為何這樣認(rèn)為。寫這篇文章并不是為了介紹一些最好的實(shí)踐 (寫這些的時(shí)候 Swift 還太年輕,還沒最好實(shí)踐被總結(jié)出來),而是舉幾個關(guān)于 Swift 強(qiáng)大之處的例子。
給大家一些我的個人背景:在成為全職 iOS/Mac 工程師之前我花了幾年的時(shí)間做 Haskell (包括一些其他函數(shù)式編程語言) 開發(fā)。我仍然認(rèn)為 Haskell 是我所有使用過的語言中最棒的之一。然而我轉(zhuǎn)戰(zhàn)到了 Objective-C,是因?yàn)槲蚁嘈?iOS 是最令人激動的平臺。剛開始接觸 Objective-C 的時(shí)候我有些許沮喪,但我慢慢地學(xué)會了欣賞它。
當(dāng)蘋果在 WWDC 發(fā)布 Swift 的時(shí)候我非常的激動。我已經(jīng)很久沒有對新技術(shù)的發(fā)布感的如此興奮了。在看過文檔之后我意識到 Swift 使我們能夠?qū)F(xiàn)有的函數(shù)式編程知識和 Cocoa API 無縫地整合到一起。我覺得這兩者的組合非常獨(dú)特:沒有任何其他的語言將它們?nèi)诤系厝绱送昝?。就?Haskell 來說,想要用它來使用 Objective-C API 相當(dāng)?shù)睦щy。同樣,想用 Objective-C 去做函數(shù)式編程也是十分困難的。
在 Utrecht 大學(xué)期間我學(xué)會了函數(shù)式編程。因?yàn)槭窃诤軐W(xué)術(shù)的環(huán)境下學(xué)習(xí)所以并沒有覺得很多復(fù)雜的術(shù)語 (moands,applicative functors 以及很多其他的東西) 有多么難懂。我覺得對很多想學(xué)習(xí)函數(shù)式編程的人來說這些名稱是一個很大的阻礙。
不僅僅名稱很不同,風(fēng)格也不一樣。作為 Objective-C 程序員,我們很習(xí)慣于面向?qū)ο缶幊?。而且因?yàn)榇蠖鄶?shù)語言不是面對對象編程就是與之類似,我們可以看懂很多不同語言的代碼。閱讀函數(shù)式編程語言的時(shí)候則大不相同 -- 如果你沒有習(xí)慣的話看起來簡直莫名其妙。
那么,為什么你要使用函數(shù)式編程呢?它很奇怪,很多人都不習(xí)慣而且學(xué)習(xí)它要花費(fèi)大量的時(shí)間。并且對于大多數(shù)問題面向?qū)ο缶幊潭寄芙鉀Q,所以沒有必要去學(xué)習(xí)任何新的東西對吧?
對于我來說,函數(shù)式編程只是工具箱中的一件工具。它是一個改變了我對編程的理解的強(qiáng)大工具。在解決問題的時(shí)候它非常強(qiáng)大。對于大多數(shù)問題面向?qū)ο缶幊潭己馨簦菍τ谄渌恍﹩栴}應(yīng)用函數(shù)式編程會給你帶來巨大的時(shí)間/精力的節(jié)省。
開始學(xué)習(xí)函數(shù)式編程或許有些痛苦。第一,你必須放手一些老的模式。而因?yàn)槲覀兒芏嗳顺D暧妹鎸ο蟮姆绞饺ニ伎?,做到這一點(diǎn)是很困難的。在函數(shù)式編程當(dāng)中你想的是不變的數(shù)據(jù)結(jié)構(gòu)以及那些轉(zhuǎn)換它們的函數(shù)。在面對對象編程當(dāng)中你考慮的是互相發(fā)送信息的對象。如果你沒有馬上理解函數(shù)式編程,這是一個好的信號。你的大腦很可能已經(jīng)完全適應(yīng)了用面對對象的方法來解決問題。
我最喜歡的 Swift 功能之一是對 optionals 的使用。Optionals 讓我們能夠應(yīng)對有可能存在也有可能不存在的值。在 Objective-C 里我們必須在文檔中清晰地說明 nil 是否是允許的。Optionals 讓我們將這份責(zé)任交給了類型系統(tǒng)。如果你有一個可選值,你就知道它可以是 nil。如果它不是可選值,你知道它不可能是 nil。
舉個例子,看看下面一小段 Objective-C 代碼
- (NSAttributedString *)attributedString:(NSString *)input
{
return [[NSAttributedString alloc] initWithString:input];
}
看上去沒有什么問題,但是如果 input 是 nil, 它就會崩潰。這種問題你只能在運(yùn)行的時(shí)候才能發(fā)現(xiàn)。取決于你如何使用它,你可能很快能發(fā)現(xiàn)問題,但是你也有可能在發(fā)布應(yīng)用之后才發(fā)現(xiàn),導(dǎo)致用戶正在使用的應(yīng)用崩潰。
用相同的 Swift 的 API 來做對比。
extension NSAttributedString {
init(string str: String)
}
看起來像對Objective-C的直接翻譯,但是 Swift 不允許 nil 被傳入。如果要達(dá)到這個目的,API 需要變成這個樣子:
extension NSAttributedString {
init(string str: String?)
}
注意新加上的問號。這意味著你可以使用一個值或者是 nil。類非常的精確:只需要看一眼我們就知道什么值是允許的。使用 optionals 一段時(shí)間之后你會發(fā)現(xiàn)你只需要閱讀類型而不用再去看文檔了。如果犯了一個錯誤,你會得到一個編譯時(shí)警告而不是一個運(yùn)行時(shí)錯誤。
如果可能的話避免使用 optionals。Optionals 對于使用你 API 的人們來說是一個多余的負(fù)擔(dān)。話雖如此,還是有很多地方可以很好使用它們。如果你有一個函數(shù)會因?yàn)橐粋€明顯的原因失敗你可以返回一個 optional。舉例來說,比如將一個 #00ff00 字符串轉(zhuǎn)換成顏色。如果你的參數(shù)不符合正確的格式,你應(yīng)該返回一個 nil 。
func parseColorFromHexString(input: String) -> UIColor? {
// ...
}
如果你需要闡明錯誤信息,你可以使用 Either 或者 Result 類型 (不在標(biāo)準(zhǔn)庫里面)。當(dāng)失敗的原因很重要的時(shí)候,這種做法會非常有用。“Error Handling in Swift” 一文中有個很好的例子。
Enums 是一個隨 Swift 推出的新東西,它和我們在 Objective-C 中見過的東西都大不相同。在 Objective-C 里面我們有一個東西叫做 enums, 但是它們差不多就是升級版的整數(shù)。
我們來看看布爾類型。一個布爾值是兩種可能性 -- true 或者 false -- 中的一個。很重要的一點(diǎn)是沒有辦法再添加另外一個值 -- 布爾類型是封閉的。布爾類型的封閉性的好處是每當(dāng)使用布爾值的時(shí)候我們只需要考慮 true 或者 false 這兩種情況。
在這一點(diǎn)上面 optionals 是一樣的。總共只有兩種情況:nil 或者有值。在 Swift 里面布爾和 optional 都可以被定義為 enums。但有一個不同點(diǎn):在 optional enum 中有一種可能性有一個相關(guān)值。我們來看看它們不同的定義:
enum Boolean {
case False
case True
}
enum Optional<A> {
case Nil
case Some(A)
}
它們非常的相似。如果你把它們的名稱改成一樣的話,那么唯一的區(qū)別就是括號里的相關(guān)值。如果你給 optional 中的 Nil 情況也加上一個值,你就會得到一個 Either 類型:
enum Either<A,B> {
case Left<A>
case Right<B>
}
在函數(shù)式編程當(dāng)中,在你想表示兩件事情之間的選擇時(shí)候你會經(jīng)常用到 Either 類型。舉個例子:如果你有一個函數(shù)返回一個整數(shù)或者一個錯誤,你就可以用 Either<Int, NSError>。如果你想在一個字典中儲存布爾值或者字符串,你就可以使用 Either<Bool,String> 作為鍵。
理論旁白:有些時(shí)候 enums 被稱為 sum 類型,因?yàn)樗鼈兪菐讉€不同類型的總和。在
Either類型的例子中,它們表達(dá)的是A類型和B類型的和。Structs 和 tuples 被稱為 product 類型,因?yàn)樗鼈兇韼讉€不同類型的乘積。參見“algebraic data types.”
理解什么時(shí)候使用 enums 什么時(shí)候使用其他的數(shù)據(jù)類型 (比如 class 或者 structs)會有一些難度。當(dāng)你有一個固定數(shù)量的值的集合的時(shí)候,enum 是最有用的。比如說,如果我們設(shè)計(jì)一個 Github API 的 wrapper,我們可以用 enum 來表示端點(diǎn)。比如有一個不需要任何參數(shù)的 /zen 的 API 端點(diǎn)。再比如為了獲取用戶的資料我們需要提供用戶名。最后我們顯示用戶的倉庫時(shí),我們需要提供用戶名以及一個值去說明是否從小到大地排列結(jié)果。
enum Github {
case Zen
case UserProfile(String)
case Repositories(username: String, sortAscending: Bool)
}
定義 API 端點(diǎn)是很好的使用 enum 的場景。API 的端點(diǎn)是有限的,所以我們可以為每一個端點(diǎn)定義一個情況。如果我們在對這些端點(diǎn)使用 switch 的時(shí)候沒有包含所有情況的話,我們會被給予警告。所以說當(dāng)我們需要添加一個情況的時(shí)候我們需要更新每一個用到這個 enum 的函數(shù)。
除非能夠拿到源代碼,其他使用我們 enum 的人不能添加新的情況,這是一個非常有用的限制。想想要是你能夠加一種新情況到 Bool 或者 Optional 里會怎么樣吧 -- 所有用到 它的函數(shù)都需要重寫。
比如說我們正在開發(fā)一個貨幣轉(zhuǎn)換器。我們可以將貨幣給定義成 enum:
enum Currency {
case Eur
case Usd
}
我們現(xiàn)在可以做一個獲取任何貨幣符號的函數(shù):
func symbol(input: Currency) -> String {
switch input {
case .Eur: return "€"
case .Usd: return "$"
}
}
最后,我們可以用我們的 symbol 函數(shù),來依據(jù)系統(tǒng)本地設(shè)置得到一個很好地格式化過的字符串:
func format(amount: Double, currency: Currency) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .CurrencyStyle
formatter.currencySymbol = symbol(currency)
return formatter.stringFromNumber(amount)
}
這樣一來有一個很大的限制。我們可能會想讓我們 API 的使用者在將來可以修改一些情況。在 Objective-C 當(dāng)中向一個接口里添加更多類型的常見解決方法是子類化。在 Objective-C 里面理論上你可以子類化任何一個類,然后通過這種辦法來擴(kuò)展它。在 Swift 里面你仍然可以使用子類化,但是只能對 class 使用,對于 enum 則不行。然而,我們可以用另一種技術(shù)來達(dá)到目的 (這種辦法在 Objetive-C 和 Swift 的 protocol 中都可行)。
假設(shè)我們定義一個貨幣符號的協(xié)議:
protocol CurrencySymbol {
func symbol() -> String
}
現(xiàn)在我們讓 Currency 類型遵守這個協(xié)議。注意我們可以將 input 參數(shù)去掉,因?yàn)檫@里它被作為 self 隱式地進(jìn)行傳遞:
extension Currency : CurrencySymbol {
func symbol() -> String {
switch self {
case .Eur: return "€"
case .Usd: return "$"
}
}
}
現(xiàn)在我們可以重寫 format 方法來格式化任何遵守我們協(xié)議的類型:
func format(amount: Double, currency: CurrencySymbol) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .CurrencyStyle
formatter.currencySymbol = currency.symbol()
return formatter.stringFromNumber(amount)
}
這樣一來我們將我們代碼的可延展性大大提升類 -- 任何遵守 CurrencySymbol 協(xié)議的類型都可以被格式化。比如說,我們建立一個新的類型來儲存比特幣,我們可以立刻讓它擁有格式化功能:
struct Bitcoin : CurrencySymbol {
func symbol() -> String {
return "B?"
}
}
這是一種寫出具有延展性函數(shù)的很好的方法。通過使用一個需要遵守協(xié)議,而不是一個實(shí)實(shí)在在的類型,你的 API 的用戶能夠加入更多的類型。你仍然可以利用 enum 的靈活性,但是通過讓它們遵守協(xié)議,你可以更好地表達(dá)自己的意思。根據(jù)你的具體情況,你現(xiàn)在可以輕松地選擇是否開放你的 API。
我認(rèn)為類型的安全性是 Swift 一個很大的優(yōu)勢。就像我們在討論 optionals 時(shí)看見的一樣,我們可以用一些聰明的手段將某些檢測從運(yùn)行時(shí)轉(zhuǎn)移到編譯時(shí)。Swift 中數(shù)組的工作方式就是一個例子:一個數(shù)組是泛型的,它只能容納一個類型的對象。將一個整數(shù)附加在一個字符組數(shù)組后面是做不到的。這樣以來就消滅了一個類的潛在 bug。(值得注意的是如果你需要同時(shí)將字符串或者整數(shù)放到一個數(shù)組里的話,你可以使用上面談到過的 Either 類型。)
再比如說,我們要將我們到貨幣轉(zhuǎn)換器延展為一個通用的單位換算器。如果我們使用 Double 去表示數(shù)量,會有一點(diǎn)點(diǎn)誤導(dǎo)性。比如說,100.0 可以表示 100 美元,100 千克或者任何能用 100 表示的東西。我們可以借助類型系統(tǒng)來制作不同的類型來表示不同的物理上的數(shù)量。比如說我們可以定義一個類型來表示錢:
struct Money {
let amount : Double
let currency: Currency
}
我們可以定義另外一個結(jié)構(gòu)來表示質(zhì)量:
struct Mass {
let kilograms: Double
}
現(xiàn)在我們就消除了不小心將 Money 和 Mass 相加的可能性?;谀銘?yīng)用的特質(zhì)有時(shí)候?qū)⒁恍┖唵蔚念愋桶b成這樣是很有效的。不僅如此,閱讀代碼也會變得更加簡單。假設(shè)我們遇到一個 pounds 函數(shù):
func pounds(input: Double) -> Double
光看類型定義很難看出來這個函數(shù)的功能。它將歐元裝換成英鎊?還是將千克轉(zhuǎn)換成磅? (英文中英鎊和磅均為 pound) 我們可以用不同的名字,或者可以建立文檔 (都是很好的辦法),但是我們有第三種選擇。我們可以將這個類型變得更明確:
func pounds(input: Mass) -> Double
我們不僅讓這個函數(shù)的用戶能夠立刻理解這個函數(shù)的功能,我們也防止了不小心傳入其他單位的參數(shù)。如果你試圖將 Money 作為參數(shù)來使用這個函數(shù),編譯器是不會接受的。另外一個可能的提升是使用一個更精確的返回值?,F(xiàn)在它只是一個 Double。
Swift 另外一個很棒的功能是內(nèi)置的不可變性。在 Cocoa 當(dāng)中很多的 API 都已經(jīng)體現(xiàn)出了不可變性的價(jià)值。想了解這一點(diǎn)為什么如此重要,“Error Handling in Swift” 是一個很好的參考。比如,作為一個 Cocoa 開發(fā)者,我們使用很多成對的類 (NSString vs. NSMutableString,NSArray vs. NSMutableArray)。當(dāng)你得到一個字符串值,你可以假設(shè)它不會被改變。但是如果你要完全確信,你依然要復(fù)制它。然后你才知道你有一份不可變的版本。
在 Swifit 里面,不可變性被直接加入這門語言。比如說如果你想建立一個可變的字符串,你可以如下的代碼:
var myString = "Hello"
然而,如果你想要一個不可變的字符串,你可以做如下的事情:
let myString = "Hello"
不可變的數(shù)據(jù)在創(chuàng)建可能會被未知用戶使用的 API 時(shí)會給你很大的幫助。比如說,你有一個需要字符串作為參數(shù)的函數(shù),在你迭代它的時(shí)候,確定它不會被改變是很重要的。在 Swift 當(dāng)中這是默認(rèn)的行為。正是因?yàn)檫@個原因,在寫多線程代碼的時(shí)候使用不可變資料會使難度大大降低。
還有另外一個巨大的優(yōu)勢。如果你的函數(shù)只使用不可變的數(shù)據(jù),你的類型簽名就會成為很好的文檔。在 Objective-C 當(dāng)中則不然。比如說,假設(shè)你準(zhǔn)備在 OS X 上使用 CIFilter。在實(shí)例化之后你需要使用 setDefaults 方法。這一點(diǎn)在文檔中有提到。有很多這樣類都是這個樣子。在實(shí)例化之后,在你使用它之前你必須要使用另外一個方法。問題在于,如果不閱讀文檔的話,經(jīng)常會不清楚哪些函數(shù)需要被使用,最后你有可能遇到很奇怪的狀況。
當(dāng)使用不可變資料的時(shí)候,類型簽名讓事情變得很清晰。比如說,map 的類簽名。我們知道有一個可選的 T 值,而且有一個將 T 轉(zhuǎn)換成 U 的函數(shù)。結(jié)果是一個可選的 U 值。原始值是不可能改變的:
func map<T, U>(x: T?, f: T -> U) -> U?
對于數(shù)組的 map 來說是一樣的。它被定義成一個數(shù)組的延伸,所以參數(shù)本身是 self。我們可以看到它用一個函數(shù)將 T 轉(zhuǎn)化成 U,并且生成一個 U 的數(shù)組。因?yàn)樗且粋€不可變的函數(shù),我們知道原數(shù)組是不會變化的,而且我們知道結(jié)果也是不會改變的。將這些限制內(nèi)置在l類型系統(tǒng)中,并有編譯器來監(jiān)督執(zhí)行,讓我們不再需要去查看文檔并記住什么會變化。
extension Array {
func map<U>(transform: T -> U) -> [U]
}
Swift 帶來了很多有趣的可能性。我尤其喜歡的一點(diǎn)是過去我們需要手動檢測或者閱讀文檔的事情現(xiàn)在編譯器可以幫我們來完成。我們可以選擇在合適的時(shí)機(jī)去使用這些可能性。我們依然會用我們現(xiàn)有的,成熟的辦法去寫代碼,但是我們可以在合適的時(shí)候在我們代碼的某些地方應(yīng)用這些新的可能性。
我預(yù)測:Swift 會很大程度上改變我們寫代碼的方式,而且是向好的方向改變。脫離 Objective-C 會需要幾年的時(shí)間,但是我相信我們中的大多數(shù)人會做出這個改變并且不會后悔。有些人會很快的適應(yīng),對另外一些人可能會花上很長的時(shí)間。但是我相信總有一天絕大多數(shù)人會看到 Swift 帶給我們的種種好處。