Swift 的強大之處

edithfang發表於2014-09-28



在寫任何東西之前我需要承認我是帶有偏見的:我愛 Swift。我認為這是從我開始接觸 Cocoa 生態系統以來這個平臺上發生的最好的事情。我想通過分享我在 Swift,Objective-C 和 Haskell 上的經驗讓大家知道我為何這樣認為。寫這篇文章並不是為了介紹一些最好的實踐 (寫這些的時候 Swift 還太年輕,還沒最好實踐被總結出來),而是舉幾個關於 Swift 強大之處的例子。

給大家一些我的個人背景:在成為全職 iOS/Mac 工程師之前我花了幾年的時間做 Haskell (包括一些其他函數語言程式設計語言) 開發。我仍然認為 Haskell 是我所有使用過的語言中最棒的之一。然而我轉戰到了 Objective-C,是因為我相信 iOS 是最令人激動的平臺。剛開始接觸 Objective-C 的時候我有些許沮喪,但我慢慢地學會了欣賞它。

當蘋果在 WWDC 釋出 Swift 的時候我非常的激動。我已經很久沒有對新技術的釋出感的如此興奮了。在看過文件之後我意識到 Swift 使我們能夠將現有的函數語言程式設計知識和 Cocoa API 無縫地整合到一起。我覺得這兩者的組合非常獨特:沒有任何其他的語言將它們融合地如此完美。就拿 Haskell 來說,想要用它來使用 Objective-C API 相當的困難。同樣,想用 Objective-C 去做函數語言程式設計也是十分困難的。

在 Utrecht 大學期間我學會了函數語言程式設計。因為是在很學術的環境下學習所以並沒有覺得很多複雜的術語 (moands,applicative functors 以及很多其他的東西) 有多麼難懂。我覺得對很多想學習函數語言程式設計的人來說這些名稱是一個很大的阻礙。

不僅僅名稱很不同,風格也不一樣。作為 Objective-C 程式設計師,我們很習慣於物件導向程式設計。而且因為大多數語言不是面對物件程式設計就是與之類似,我們可以看懂很多不同語言的程式碼。閱讀函數語言程式設計語言的時候則大不相同 — 如果你沒有習慣的話看起來簡直莫名其妙。

那麼,為什麼你要使用函數語言程式設計呢?它很奇怪,很多人都不習慣而且學習它要花費大量的時間。並且對於大多數問題物件導向程式設計都能解決,所以沒有必要去學習任何新的東西對吧?

對於我來說,函數語言程式設計只是工具箱中的一件工具。它是一個改變了我對程式設計的理解的強大工具。在解決問題的時候它非常強大。對於大多數問題物件導向程式設計都很棒,但是對於其他一些問題應用函數語言程式設計會給你帶來巨大的時間/精力的節省。

開始學習函數語言程式設計或許有些痛苦。第一,你必須放手一些老的模式。而因為我們很多人常年用面對物件的方式去思考,做到這一點是很困難的。在函數語言程式設計當中你想的是不變的資料結構以及那些轉換它們的函式。在面對物件程式設計當中你考慮的是互相傳送資訊的物件。如果你沒有馬上理解函數語言程式設計,這是一個好的訊號。你的大腦很可能已經完全適應了用面對物件的方法來解決問題。

例子

我最喜歡的 Swift 功能之一是對 optionals 的使用。Optionals 讓我們能夠應對有可能存在也有可能不存在的值。在 Objective-C 裡我們必須在文件中清晰地說明 nil 是否是允許的。Optionals 讓我們將這份責任交給了型別系統。如果你有一個可選值,你就知道它可以是 nil。如果它不是可選值,你知道它不可能是 nil。

舉個例子,看看下面一小段 Objective-C 程式碼
- (NSAttributedString *)attributedString:(NSString *)input 
{
    return [[NSAttributedString alloc] initWithString:input];
}
看上去沒有什麼問題,但是如果 input 是 nil, 它就會崩潰。這種問題你只能在執行的時候才能發現。取決於你如何使用它,你可能很快能發現問題,但是你也有可能在釋出應用之後才發現,導致使用者正在使用的應用崩潰。

用相同的 Swift 的 API 來做對比。
extension NSAttributedString {  
    init(string str: String)
}
看起來像對Objective-C的直接翻譯,但是 Swift 不允許 nil 被傳入。如果要達到這個目的,API 需要變成這個樣子:
extension NSAttributedString {  
    init(string str: String?)
}
注意新加上的問號。這意味著你可以使用一個值或者是 nil。類非常的精確:只需要看一眼我們就知道什麼值是允許的。使用 optionals 一段時間之後你會發現你只需要閱讀型別而不用再去看文件了。如果犯了一個錯誤,你會得到一個編譯時警告而不是一個執行時錯誤。

建議

如果可能的話避免使用 optionals。Optionals 對於使用你 API 的人們來說是一個多餘的負擔。話雖如此,還是有很多地方可以很好使用它們。如果你有一個函式會因為一個明顯的原因失敗你可以返回一個 optional。舉例來說,比如將一個 #00ff00 字串轉換成顏色。如果你的引數不符合正確的格式,你應該返回一個 nil 。
func parseColorFromHexString(input: String) -> UIColor? {  
    // ...
}
如果你需要闡明錯誤資訊,你可以使用 Either 或者 Result 型別 (不在標準庫裡面)。當失敗的原因很重要的時候,這種做法會非常有用。“Error Handling in Swift” 一文中有個很好的例子。

Enums

Enums 是一個隨 Swift 推出的新東西,它和我們在 Objective-C 中見過的東西都大不相同。在 Objective-C 裡面我們有一個東西叫做 enums, 但是它們差不多就是升級版的整數。

我們來看看布林型別。一個布林值是兩種可能性 — true 或者 false — 中的一個。很重要的一點是沒有辦法再新增另外一個值 — 布林型別是封閉的。布林型別的封閉性的好處是每當使用布林值的時候我們只需要考慮 true 或者 false 這兩種情況。

在這一點上面 optionals 是一樣的。總共只有兩種情況:nil 或者有值。在 Swift 裡面布林和 optional 都可以被定義為 enums。但有一個不同點:在 optional enum 中有一種可能性有一個相關值。我們來看看它們不同的定義:
enum Boolean {  
    case False
    case True
}
 
enum Optional<A> {  
    case Nil
    case Some(A)
}
它們非常的相似。如果你把它們的名稱改成一樣的話,那麼唯一的區別就是括號裡的相關值。如果你給 optional 中的 Nil 情況也加上一個值,你就會得到一個 Either 型別:
enum Either<A,B> {  
    case Left<A>
    case Right<B>
}
在函數語言程式設計當中,在你想表示兩件事情之間的選擇時候你會經常用到 Either 型別。舉個例子:如果你有一個函式返回一個整數或者一個錯誤,你就可以用 Either<Int, NSError>。如果你想在一個字典中儲存布林值或者字串,你就可以使用Either<Bool,String> 作為鍵。
理論旁白:有些時候 enums 被稱為 sum 型別,因為它們是幾個不同型別的總和。在 Either 型別的例子中,它們表達的是 A型別和 B 型別的和。Structs 和 tuples 被稱為 product 型別,因為它們代表幾個不同型別的乘積。參見“algebraic data types.”
理解什麼時候使用 enums 什麼時候使用其他的資料型別 (比如 class 或者 structs)會有一些難度。當你有一個固定數量的值的集合的時候,enum 是最有用的。比如說,如果我們設計一個 Github API 的 wrapper,我們可以用 enum 來表示端點。比如有一個不需要任何引數的 /zen的 API 端點。再比如為了獲取使用者的資料我們需要提供使用者名稱。最後我們顯示使用者的倉庫時,我們需要提供使用者名稱以及一個值去說明是否從小到大地排列結果。
enum Github {  
    case Zen
    case UserProfile(String)
    case Repositories(username: String, sortAscending: Bool)
}
定義 API 端點是很好的使用 enum 的場景。API 的端點是有限的,所以我們可以為每一個端點定義一個情況。如果我們在對這些端點使用 switch 的時候沒有包含所有情況的話,我們會被給予警告。所以說當我們需要新增一個情況的時候我們需要更新每一個用到這個 enum 的函式。

除非能夠拿到原始碼,其他使用我們 enum 的人不能新增新的情況,這是一個非常有用的限制。想想要是你能夠加一種新情況到Bool 或者 Optional 裡會怎麼樣吧 — 所有用到 它的函式都需要重寫。

比如說我們正在開發一個貨幣轉換器。我們可以將貨幣給定義成 enum:
enum Currency {  
    case Eur
    case Usd
}
我們現在可以做一個獲取任何貨幣符號的函式:
func symbol(input: Currency) -> String {  
    switch input {
        case .Eur: return "€"
        case .Usd: return "[        ubbcodeplace_9        ]quot;
    }
}
最後,我們可以用我們的 symbol 函式,來依據系統本地設定得到一個很好地格式化過的字串:
func format(amount: Double, currency: Currency) -> String {  
    let formatter = NSNumberFormatter()
    formatter.numberStyle = .CurrencyStyle
    formatter.currencySymbol = symbol(currency)
    return formatter.stringFromNumber(amount)
}
這樣一來有一個很大的限制。我們可能會想讓我們 API 的使用者在將來可以修改一些情況。在 Objective-C 當中向一個介面裡新增更多型別的常見解決方法是子類化。在 Objective-C 裡面理論上你可以子類化任何一個類,然後通過這種辦法來擴充套件它。在 Swift 裡面你仍然可以使用子類化,但是隻能對 class 使用,對於 enum 則不行。然而,我們可以用另一種技術來達到目的 (這種辦法在 Objetive-C 和 Swift 的 protocol 中都可行)。

假設我們定義一個貨幣符號的協議:
protocol CurrencySymbol {  
    func symbol() -> String
}
現在我們讓 Currency 型別遵守這個協議。注意我們可以將 input 引數去掉,因為這裡它被作為 self 隱式地進行傳遞:
extension Currency : CurrencySymbol {  
   func symbol() -> String {
        switch self {
            case .Eur: return "€"
            case .Usd: return "[        ubbcodeplace_12        ]quot;
        }
    }
}
現在我們可以重寫 format 方法來格式化任何遵守我們協議的型別:
func format(amount: Double, currency: CurrencySymbol) -> String {  
    let formatter = NSNumberFormatter()
    formatter.numberStyle = .CurrencyStyle
    formatter.currencySymbol = currency.symbol()
    return formatter.stringFromNumber(amount)
}
這樣一來我們將我們程式碼的可延展性大大提升類 — 任何遵守 CurrencySymbol 協議的型別都可以被格式化。比如說,我們建立一個新的型別來儲存比特幣,我們可以立刻讓它擁有格式化功能:
struct Bitcoin : CurrencySymbol {  
    func symbol() -> String {
        return "B⃦"
    }
}
這是一種寫出具有延展性函式的很好的方法。通過使用一個需要遵守協議,而不是一個實實在在的型別,你的 API 的使用者能夠加入更多的型別。你仍然可以利用 enum 的靈活性,但是通過讓它們遵守協議,你可以更好地表達自己的意思。根據你的具體情況,你現在可以輕鬆地選擇是否開放你的 API。

型別安全

我認為型別的安全性是 Swift 一個很大的優勢。就像我們在討論 optionals 時看見的一樣,我們可以用一些聰明的手段將某些檢測從執行時轉移到編譯時。Swift 中陣列的工作方式就是一個例子:一個陣列是泛型的,它只能容納一個型別的物件。將一個整數附加在一個字元組陣列後面是做不到的。這樣以來就消滅了一個類的潛在 bug。(值得注意的是如果你需要同時將字串或者整數放到一個陣列裡的話,你可以使用上面談到過的 Either 型別。)

再比如說,我們要將我們到貨幣轉換器延展為一個通用的單位換算器。如果我們使用 Double去表示數量,會有一點點誤導性。比如說,100.0 可以表示 100 美元,100 千克或者任何能用 100 表示的東西。我們可以藉助型別系統來製作不同的型別來表示不同的物理上的數量。比如說我們可以定義一個型別來表示錢:
struct Money {  
    let amount : Double
    let currency: Currency
}
我們可以定義另外一個結構來表示質量:
struct Mass {  
    let kilograms: Double
}
現在我們就消除了不小心將 Money 和 Mass 相加的可能性。基於你應用的特質有時候將一些簡單的型別包裝成這樣是很有效的。不僅如此,閱讀程式碼也會變得更加簡單。假設我們遇到一個pounds 函式:
func pounds(input: Double) -> Double
光看型別定義很難看出來這個函式的功能。它將歐元裝換成英鎊?還是將千克轉換成磅? (英文中英鎊和磅均為 pound) 我們可以用不同的名字,或者可以建立文件 (都是很好的辦法),但是我們有第三種選擇。我們可以將這個型別變得更明確:
func pounds(input: Mass) -> Double
我們不僅讓這個函式的使用者能夠立刻理解這個函式的功能,我們也防止了不小心傳入其他單位的引數。如果你試圖將 Money 作為引數來使用這個函式,編譯器是不會接受的。另外一個可能的提升是使用一個更精確的返回值。現在它只是一個 Double。

不可變性

Swift 另外一個很棒的功能是內建的不可變性。在 Cocoa 當中很多的 API 都已經體現出了不可變性的價值。想了解這一點為什麼如此重要,“Error Handling in Swift” 是一個很好的參考。比如,作為一個 Cocoa 開發者,我們使用很多成對的類 (NSStringvs.NSMutableString,NSArray vs. NSMutableArray)。當你得到一個字串值,你可以假設它不會被改變。但是如果你要完全確信,你依然要複製它。然後你才知道你有一份不可變的版本。

在 Swifit 裡面,不可變性被直接加入這門語言。比如說如果你想建立一個可變的字串,你可以如下的程式碼:
var myString = "Hello"
然而,如果你想要一個不可變的字串,你可以做如下的事情:
let myString = "Hello"
不可變的資料在建立可能會被未知使用者使用的 API 時會給你很大的幫助。比如說,你有一個需要字串作為引數的函式,在你迭代它的時候,確定它不會被改變是很重要的。在 Swift 當中這是預設的行為。正是因為這個原因,在寫多執行緒程式碼的時候使用不可變資料會使難度大大降低。

還有另外一個巨大的優勢。如果你的函式只使用不可變的資料,你的型別簽名就會成為很好的文件。在 Objective-C 當中則不然。比如說,假設你準備在 OS X 上使用 CIFilter。在例項化之後你需要使用 setDefaults 方法。這一點在文件中有提到。有很多這樣類都是這個樣子。在例項化之後,在你使用它之前你必須要使用另外一個方法。問題在於,如果不閱讀文件的話,經常會不清楚哪些函式需要被使用,最後你有可能遇到很奇怪的狀況。

當使用不可變資料的時候,型別簽名讓事情變得很清晰。比如說,map 的類簽名。我們知道有一個可選的 T 值,而且有一個將 T 轉換成 U 的函式。結果是一個可選的 U 值。原始值是不可能改變的:
func map<T, U>(x: T?, f: T -> U) -> U?
對於陣列的 map 來說是一樣的。它被定義成一個陣列的延伸,所以引數本身是 self。我們可以看到它用一個函式將 T 轉化成 U,並且生成一個 U 的陣列。因為它是一個不可變的函式,我們知道原陣列是不會變化的,而且我們知道結果也是不會改變的。將這些限制內建在l型別系統中,並有編譯器來監督執行,讓我們不再需要去檢視文件並記住什麼會變化。
extension Array {  
    func map<U>(transform: T -> U) -> [U]
}
總結

Swift 帶來了很多有趣的可能性。我尤其喜歡的一點是過去我們需要手動檢測或者閱讀文件的事情現在編譯器可以幫我們來完成。我們可以選擇在合適的時機去使用這些可能性。我們依然會用我們現有的,成熟的辦法去寫程式碼,但是我們可以在合適的時候在我們程式碼的某些地方應用這些新的可能性。

我預測:Swift 會很大程度上改變我們寫程式碼的方式,而且是向好的方向改變。脫離 Objective-C 會需要幾年的時間,但是我相信我們中的大多數人會做出這個改變並且不會後悔。有些人會很快的適應,對另外一些人可能會花上很長的時間。但是我相信總有一天絕大多數人會看到 Swift 帶給我們的種種好處。
來自:IT江湖
相關閱讀
評論(1)

相關文章