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