可選值(Optionals)是 Swift 引入的一項非常棒的特性,本文將基於原書中的案例以及函數語言程式設計思想進行討論。
概述
首先推薦大家閱讀原書中的可選值章節,有很多使用細節以及與 Objective-C 的對比討論,我認為如果將這一章單獨拿出來會是一篇講解 Swift 可選值很不錯的文章。
關於 Swift Optionals,我曾寫過一篇《Swift Optionals 原始碼解析》,試圖藉助 Swift 原始碼來解析可選值,其中開篇提到:
Swift 引入的 Optionals,很好的解決了 Objective-C 時代 “nil or not nil” 的問題,配合 Type-Safe 特性,幫我們減少了很多隱藏的問題。
這裡主要有兩方面意思:
- “nil or not nil”:在 Objective-C 中,
nil
是一個極其常見的值,指向一個為空的物件,很重要的是向nil
發訊息是安全的,回憶一下我們曾經寫過的程式碼,有意無意的都使用了該特性,換句話說,有很多情況我們是不對為空做區別處理的,這樣一來,我們的程式碼通常會簡潔一些,可是一旦由於nil
的存在或是傳遞導致了 Crash,就不得不去追查任何可能為nil
的情況。 - “Type-Safe”:Swift 是一個型別安全的語言,我們需要清楚的瞭解被操作的值的型別,編譯器會對程式碼時進行型別檢查,把不匹配的型別標記為錯誤,這樣能夠幫助我們儘早的發現程式中的問題。而對於類似 Objective-C 中的
nil
來說,一定程度與型別安全產生了衝突,因而,引入 Optionals 來與 Type-Safe 配合很好的解決這一問題。
下面將結合原書案例進一步討論。
注:本文將不涉及對可選值基礎應用的講解,如需學習,請查閱原書、官方文件或《Swift Optionals 原始碼解析》。
案例:Dictionary
使用兩個 Dictionary
分別儲存國家及其首都、城市名及其人口數量:
let capitals = [
"France": "Paris",
"Spain": "Madrid",
"The Netherlands": "Amsterdam",
"Belgium": "Brussels"
]
let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562] // 單位為“千”複製程式碼
問題:編寫函式接收輸入的國家名,輸出其首都城市的人口數量。
與之前章節一樣,問題本身並不複雜,為了對比,我們先編寫一個 Objective-C 版本的函式:
@property (nonatomic, copy) NSDictionary *capitals;
@property (nonatomic, copy) NSDictionary *cities;
_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
@"Paris", @"France",
@"Madrid", @"Spain",
@"Amsterdam", @"The Netherlands",
@"Brussels", @"Belgium",
nil];
_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
@2241, @"Paris",
@3165, @"Madrid",
@827, @"Amsterdam",
@3562, @"Berlin",
nil];
- (NSInteger)populationOfCapital:(NSString *)country {
return [_cities[_capitals[country]] integerValue] * 1000;
}複製程式碼
populationOfCapital
函式就是我們的解決方案,只需要一行程式碼即可,下面測試一下:
// Case 1
NSLog(@"%ld", [self populationOfCapital:@"France"]); // 2241000
// Case 2
NSLog(@"%ld", [self populationOfCapital:@"China"]); // 0複製程式碼
對於 Case 1 一切正常,而對於 Case 2,由於 _capitals
中不包含 China
,所以 _capitals[country]
為 nil
,按照 Objective-C 的 nil
處理辦法,我們得到了 0
,但是,我們無法區分這個是正常的結果還是異常的輸出,如果我們修改一下 _capitals
和 _cities
:
_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
@"Paris", @"France",
@"Madrid", @"Spain",
@"Amsterdam", @"The Netherlands",
@"Brussels", @"Belgium",
@"Tokyo", @"Japan", // Add Japan
nil];
_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
@2241, @"Paris",
@3165, @"Madrid",
@827, @"Amsterdam",
@3562, @"Berlin",
@0, @"Tokyo", // Add Tokyo
nil];
// Case 3
NSLog(@"%ld", [self populationOfCapital:@"Japan"]); // 0複製程式碼
我們忽略 Tokyo
是不是真的“零人口”,Case 3 輸出 0
是正確的,但我們無法區分 Case 2 和 Case 3 的輸出。這也就是 “nil or not nil” 問題。
那麼 Swift 如何使用 Optional 解決這個問題呢?我們知道 populationOfCapital
函式應該能夠區分 nil
和 not nil
的情況,而這正好匹配了可選值的定義:
// Optional.swift(swift/stdlib/public/core/Optional.swift)
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none // nil
case some(Wrapped) // not nil
}複製程式碼
因此,populationOfCapital
的返回值型別應為 Int?
,capitals
或 cities
查詢失敗時對應 nil
的情況,查詢成功則對應 not nil
,所以 Swift 中 populationOfCapital
函式定義如下:
func populationOfCapital(country: String) -> Int? {
if let capital = capitals[country] {
if let population = cities[capital] {
return population * 1000
} else {
return nil
}
} else {
return nil
}
}
let cities = [
"Paris": 2241,
"Madrid": 3165,
"Amsterdam": 827,
"Berlin": 3562,
"Tokyo": 0
]
let capitals = [
"France": "Paris",
"Spain": "Madrid",
"The Netherlands": "Amsterdam",
"Belgium": "Brussels",
"Japan": "Tokyo"
]
// Case 4
populationOfCapital(country: "France") // 2241000
populationOfCapital(country: "China") // nil
populationOfCapital(country: "Japan") // 0複製程式碼
可以看到,藉助可選值,Case 4 的輸出滿足了我們的需求,China 和 Japan 的情況也得到了區分。問題已經解決了,不過 populationOfCapital
完成的不夠“美觀”,也並沒有利用到函數語言程式設計思想,下面對它進行優化。
首先,我們先來看看 populationOfCapital
到底要完成什麼工作,本質上就是從一個值轉變為另一個值,即從國家名轉變為人口數量,只是在轉變過程中可能存在“失敗”的情況,需要對這些“失敗”的情況作出判斷,通過第三章,我們知道值的轉變對應的就是 map
方法,所以我們需要開發的工具庫就是支援可選值的 map
方法,稱之為:flatMap
。
要為 Optional 新增 flatMap
函式,並且存在“失敗”的情況,所以 flatMap
函式的輸出型別應為 U?
,而輸入,則應該是該可選值不為 nil
時所要做的轉換(轉換本身也是一個函式):
extension Optional {
func flatMap<U>(f: Wrapped -> U?) -> U? {
guard let x = self else {
return nil
}
return f(x)
}
}複製程式碼
工具庫就緒後,我們可以改寫 populationOfCapital
函式:
func populationOfCapital_map(country: String) -> Int? {
return capitals[country].flatMap { capital in
return cities[capital]
}.flatMap { population in
return population * 1000
}
}
// Case 5
populationOfCapital_map(country: "France") // 2241000
populationOfCapital_map(country: "China") // nil
populationOfCapital_map(country: "Japan") // 0複製程式碼
藉助工具庫,我們不再需要進行多次判斷,而是直接關注“成功”的情況即可。事實上,在 Optional 類庫中已經存在一個同樣功能的 flatMap
函式,開發中直接呼叫即可,其原始碼如下:
// Optional.swift(swift/stdlib/public/core/Optional.swift)
public func flatMap<U>(_ transform: (Wrapped) throws -> U?)
rethrows -> U? {
switch self {
case .some(let y):
return try transform(y)
case .none:
return .none
}
}複製程式碼
為什麼使用可選值?
原書中詳細討論了為什麼 Swift 使用可選值,大致總結以下兩點:
- 安全特性:不同於 Objective-C 預設近似零的處理方式,Swift 通過顯式的可選型別以及型別安全特性,幫助避免由於缺失值而導致的意外崩潰或是邏輯含糊;
- 明確的函式簽名:Objective-C 中函式的引數並無是否為空的強制要求,我們在使用時往往也比較模糊,彷彿空和非空均支援(這種情況目前有所改觀,藉助
nullable
、NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
能夠幫助我們設計空和非空的情況),而由於可選值的存在,使得 Swift 中的函式簽名是否支援nil
變得非常清晰,更有利於我們寫出健壯的程式碼。
參考資料
- Github: objcio/functional-swift
- The Swift Programming Language: The Basics
- The Swift Programming Language (Source Code)
本文屬於《函式式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!