對各種值為"空"的情況處理不當,幾乎是所有Bug的來源。
在其它編程語言里,空值的表達方式多種多樣:"" / nil / NULL / 0 / nullptr 都是我們似曾相識的表達空值的方法。
而當我們訪問一個變量時,我們有太多情況無法意識到一個變量有可能為空,進而最終在程序中埋藏了一個個閃退的隱患。
因此,Swift里,明確區分了"變量"和"值有可能為空的變量"這兩種情況,以時刻警告你:"哦,它的值有可能為空,我應該謹慎處理它。
而對于后者,謹慎不僅僅是精神層面的,Swift還從語法層面上,幫助你在處理空值時,游刃有余。
NSString *tmp = nil;
if ([tmp rangeOfString: @"Swift"].location != NSNotFound) {
// Will print out for nil string
NSLog(@"Something about swift");
}
在我們的例子里,盡管tmp
的值是nil
,但調用tmp
的rangeOfString
方法卻是合法的,它會返回一個值為0的NSRange
,因此,location
的值也是0。
但是,NSNotFound
的值卻是NSIntegerMax
。于是,盡管tmp
的值為ni
l,我們還可以在控制臺看到_Something about swift_
這樣的輸出。
那么Swift
中是怎么解決的呢?
Swift的方法,通過把不同的結果放在一個enum里,
Swift可以通過編譯器,強制我們明確處理函數返回的異常情況。
Optional關鍵實現技術模擬
讓編譯器強制我們處理可能發生錯誤的情況。為了做到這點,我們得滿足下面這幾個條件:
1、 首先,作為一個函數的返回值,它仍舊得是一個獨立的類型;
2、 其次,對于所有成功的情況,這個類型得有辦法包含正確的結果;
3、 最后,對于所有錯誤的情況,這個類型得有辦法用一個和正確情況類型不同的值來表達;
4、 做到這些,當我們把一個錯誤情況的值用在正常的業務邏輯之后,編譯器就可以由于類型錯誤,給我們予以警告了。
讓編譯器強制你處理錯誤的情況
說到這,我們應該就有思路了,一個包含兩個case的enum正是解決這個問題的完美方案:
enum Optional<T> {
case some(T) //對于所有成功的情況,我們用case some,并且把成功的結果保存在associated value里;
case none 對于所有錯誤的情況,我們用case none來表示;
}
然后,我們可以給Array添加一個和std::find類似的方法:
extension Array where Element: Equatable {
func find(_ element: Element) -> Optional<Index> {
var index = startIndex
while index != endIndex {
if self[index] == element {
return .some(index)
}
formIndex(after: &index)
}
return .none
}
}
在find
的實現里,它有兩個退出函數的路徑。當在Array
中找到參數時,就把對應的Index
作為.some
的associated value
并返回;否則,當while
循環結束時,就返回.none
。這樣,當我們用find
查找元素位置時:
var numbers = [1, 2, 3]
let index = numbers.find(4)
print(type(of: index)) // Optinal<Int>
index
的類型就會變成Optional<Int>
,于是,當我們嘗試把這個類型傳遞給remove(at:)
時:
numbers.remove(at: index) // !!! Compile time error !!!
就會直接看到一個編譯器錯誤:
為了使用index
中的值,我們只能這樣:
switch index {
case .some(let index):
numbers.remove(at: index)
case .none:
print("Not exist")
}
看到了么?只要會發生錯誤的函數返回Optional
,編譯器就會強制我們對調用成功和失敗的情況明確分開處理。并且,當你看到一個函數返回了Optional
,從它的簽名就可以知道,Hmmm,調用它有可能會發生錯誤,我得小心處理。
實際上,你并不需要自己定義這樣的Optional
類型,Swift
中的optional
變量就是如此實現的,因此,當讓find
直接返回一個Index optional
時:
func find(_ element: Element) -> Index? {
// ...
}
理解Swift
對optional
類型進行的簡化處理
Optional
作為Swift
中最重要的語言特性之一,為了避免讓你每次都通過.some
和.none
來處理不同的情況(畢竟,這是optional
的實現細節),Swift
在語法層面對這個類型做了諸多改進。
首先,optional
包含的.some
關聯值會在必要的時候,被自動升級成optional
;而nil
字面值則會被轉換成.none
。因此,我們之前的find
可以被實現成這樣:
func find(_ element: Element) -> Index? {
var index = startIndex
while index != endIndex {
if self[index] == element {
return index // Simplified for .some(index)
}
formIndex(after: &index)
}
return nil // Simplified for .none
}
注意find
中的兩個return
語句,你就能理解從字面值自動升級到optional
的含義了。實際上Array你也無需自己實現這樣的find
,Array
中自帶了一個index(of:)
方法,它的功能和實現方式,和find
是一樣的。
其次,在switch
中使用optional 可選值類型?
值的時候,我們也不用明確使用.some
和.none
,Swift
同樣做了類似的簡化:
switch index {
case let index?:
numbers.remove(at: index)
case nil:
print("Not exist")
}
我們可以用case let index
?這樣的形式來簡化讀取.some
的關聯值,用case nil
來簡化case .none
。
有哪些常用的optional
使用范式
if let
如果我們要表達“當optional
不等于nil
時,則執行某些操作”這樣的語義,最樸素的寫法,是這樣的:
let number: Int? = 1
if number != nil {
print(number!)
}
其中,number!
這樣的寫法叫做force unwrapping
,用于強行讀取optional
變量中的值,此時,如果optional
的值為nil
就會觸發運行時錯誤。所以,通常,我們會事先判斷optional
的值是否為nil
。
但這樣寫有一個弊端,如果我們需要在if代碼塊中包含多個訪問number
的語句,就要在每一處使用number!
,這顯得很啰嗦。我們明知此時number
的值不為nil
,應該可以直接使用它的值才對。為此,Swift
提供了if let
的方式,像這樣:
if let number = number {
print(number)
}
在上面的代碼里,我們使用if let
直接在if
代碼塊內部,定義了一個新的變量number
,它的值是之前number?
的值。然后,我們就可以在if
代碼塊內部,直接通過新定義的number來訪問之前number?
的值了。
這里用了一個小技巧,就是在if let
后面新定義變量的名字,和之前的optional
是一樣的。這不僅讓代碼看上去就像是訪問optional
自身一樣,而且,通常為一個optional
的值另取一個新的名字,也著實沒什么必要。
除了可以直接在if let
中綁定optional
的value
,我們還可以通過布爾表達式進一步約束optional
的值,這也是一個常見的用法,例如,我們希望number為奇數:
if let number = number, number % 2 != 0 {
print(number)
}
我們之前講到過逗號操作符在if
中的用法,在這里,number % 2 != 0
中的number
,指的是在if
代碼塊中新定義的變量,理解了這點,上面的代碼就不存在任何問題了。
有了optional
的這種用法之后,對于那些需要一連串有可能失敗的行為都成功時才執行的動作,只要這些行為都返回optional
,我們就有了一種非常漂亮的解決方法。
例如,為了從某個url加載一張jpg的圖片,我們可以這樣:
if let url = URL(string: imageUrl), url.pathExtension == "jpg",
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
let view = UIImageView(image: image)
}
在上面的例子里,從生成URL
對象,到根據url
創建Data
,到用data
創建一個UIImage
,每一步的繼續都依賴于前一步的成功,而每一步調用的方法又都返回一個optional
,因此,通過串聯多個if let,我們就把每一步成功的結果綁定在了一個新的變量上并傳遞給下一步,這樣,比我們在每一步不斷的去判斷optional
是否為nil
簡單多了。
while let
除了在條件分支中使用let
綁定optional
,我們也可以在循環中,使用類似的形式。例如,為了遍歷一個數組,我們可以這樣:
let numbers = [1, 2, 3, 4, 5, 6]
var iterator = numbers.makeIterator()
while let element = iterator.next() {
print(element)
}
在這里,iterator.next()
會返回一個Optional<Int>
,直到數組的最后一個元素遍歷完之后,會返回nil
。然后,我們用while let
綁定了數組中的每一個值,并把它們打印在了控制臺上。
看到這里,你可能會想,直接用一個for...in...
數組不就好了么?為什么要使用這種看上去有點兒麻煩的while
呢?
實際上,通過這個例子,我們要說明一個重要的問題:在Swift里,for...in
循環是通過while
模擬出來的,這也就意味著,for
循環中的循環變量在每次迭代的時候,都是一個全新的對象,而不是對上一個循環變量的修改:
for element in numbers {
print(element)
}
在上面這個for...in
循環里,每一次迭代,element
都是一個全新的對象,而不是在循環開始創建了一個element
之后,不斷去修改它的值。用while
的例子去理解,每一次for
循環迭代中的element
,就是一個新的while let
綁定。
然而,為什么要這樣做呢?
因為這樣的形式,可以彌補由于closure
捕獲變量帶來的一個不算是bug
,卻也有違直覺的問題。首先,我們來看一段JavaScript代碼:
var fnArray = [];
for (var i in [0, 1, 2]) {
fnArray[i] = () => { console.log(i); };
}
fnArray[0](); // 2
fnArray[1](); // 2
fnArray[2](); // 2
對于末尾的三個fnArray
調用,你期望會返回什么結果呢?我們在每一次for...in
循環中,定義了一個打印循環變量i的箭頭函數。當它們執行的時候,也許你會不假思索的脫口而出:當然是輸出0, 1, 2啊。
但實際上,由于循環變量i自始至終都是同一個變量,在最后調用fnArray
中保存的每一個函數時,它們在真正執行時訪問的,也都是同一個變量i。因此,這三個調用打印出來的值,都是2。類似這樣的問題,稍不注意,就會在代碼中,埋下Bug的隱患。
因此,在Swift
的for
循環里,每一次循環變量都是一個“新綁定”的結果,這樣,無論任何時間調用這個clousre
,都不會出現類似JavaScript中的問題了。
我們把之前的那個例子,用Swift
重寫一下:
var fnArray: [()->()] = []
for i in 0...2 {
fnArray.append({ print(i) })
}
fnArray[0]() // 0
fnArray[1]() // 1
fnArray[2]() // 2
這里,由于變量i在每次循環都是一個新綁定的結果,因此,每一次添加到fnArray
中的clousre
捕獲到的變量都是不同的對象。當我們分別調用它們的時候,就可以得到捕獲到它們的時候,各自的值了。
使用guard
簡化optional unwrapping
通常情況下,我們只能在optional
被unwrapping
的作用域內,來訪問它的值。
理解optional unwrapping
的作用域
例如,在下面這個arrayProcess
函數里:
func arrayProcess(array: [Int]) {
if let first = array.first {
print(first)
}
}
我們只能在if
代碼塊內部,訪問被unwrapping
之后的值。但這樣做有一個麻煩,就是如果我們要在函數內部的多個地方使用array.first
,就要在每個地方都進行某種形式的unwrapping
,這不僅寫起來很麻煩,還會讓代碼看上去非常凌亂。
實際上,面對這種在多處訪問同一個optional
的情況,更多的時候,我們需要的是一個確保optional
一定不為nil
的環境。如果,我們能在一個地方統一處理optioanl
為nil
的情況,就可以在這個地方之外,安全的訪問optional
的值了。
好在,Swift
在語法上,對這個操作進行了支持,這就是guard
的用法:
func arrayProcess(array: [Int]) {
guard let first = array.first else {
return
}
print(first)
}
在上面的例子里,我們使用guard let
綁定了array.first
的非nil
值。如果array.first
為nil
,就會轉而執行else
代碼塊里的內容。這樣,我們就可以在else
內部,統一處理array.first
為nil
的情況。在這里,我們可以編寫任意多行語句,唯一的要求,就是else
的最后一行必須離開當前作用域,對于函數來說,就是從函數返回,或者調用fatalError
表示一個運行時錯誤。
而這,也是為數不多的,我們可以在value binding
作用域外部,來訪問optional value
的情況。
一個特殊情況
在Swift
里,有一類特殊的函數,它們返回Never
,表示這類方法直到程序執行結束都不會返回。Swift
管這種類型叫做uninhabited type
。
什么情況會使用Never
呢?其實并不多,一種是崩潰前,例如,使用fatalError
返回一些用于排錯的消息;另一種,是類似dispatchMain
這樣,在進程生命周期中一直需要執行的方法。
當我們在返回Never
的函數中,使用guard
時,else
語句并不需要離開當前作用域,而是最后一行必須調用另外一個返回Never
的函數就好了。例如下面的例子:
func toDo(item: String?) -> Never {
guard let item = item else {
fatalError("Nothing to do")
}
fatalError("Implement \(item) later")
}
在toDo
的實現里,如果我們沒有指定要完成的內容,就在else
里調用fatalError
顯示一個錯誤。在這里,fatalError
也是一個返回Never
的函數。
一個偽裝的optional
除了使用真正的optional
變量之外,有時,我們還是利用編譯器對optional
的識別機制來為變量的訪問創造一個安全的使用環境。例如,為了把數組中第一個元素轉換為String
,我們可以這樣:
func arrayProcess(array: [Int]) -> String? {
let firstNumber: Int
if let first = array.first {
firstNumber = first
} else {
return nil
}
// `firstNumber` could be used here safely
return String(firstNumber)
}
在上面的代碼里,有兩點值得說明:
首先,我們使用了Swift
中延遲初始化的方式,在if let
中,才初始化常量firstNumber
;
其次,從程序的執行路徑分析,對于firstNumber
來說,要不我們已經在if let
中完成了初始化;要不,我們已經從else
返回。因此,只要程序的執行邏輯來到了if...else...
之后,訪問firstNumber
就一定是安全的了。
實際上,Swift
編譯器也可以識別這樣的執行邏輯。firstNumber
就像一個偽裝的optional
一樣,在if let
分支里被初始化成具體的值,在else
分支里,被認為值是nil
。因此,在else
代碼塊之后,就像在之前guard
語句之后一樣,我們也可以認為firstNumber
一定是包含值的,因此安全的訪問它。
通常,當我們要調用一個包含在optional
中的對象的方法時,我們可能會像下面這樣把兩種情況分開處理:
var swift: String? = "Swift"
let SWIFT: String
if let swift = swift {
SWIFT = swift.uppercased()
}
else {
fatalError("Cannot uppercase a nil")
}
但是,當我們僅僅想獲得一個包含結果的optional類型時,上面的寫法就顯得有點兒啰嗦了。實際上,我們有更簡單的用法:
let SWIFT = swift?.uppercased() // Optional("SWIFT")
這樣,我們就會得到一個新的Optional
。并且,我們還可以把optional
對象的方法調用串聯起來:
let SWIFT = swift?.uppercased().lowercased()
// Optional("swift")
上面的形式,在Swift
里,就叫做optional chaining
。只要前一個方法返回optiona
l類型,我們就可以一直把調用串聯下去。但是,如果你仔細觀察上面的串聯方法,卻可以發現一個有趣的細節:對于第一個optional
,我們調用uppercased()
方法使用的是?.
操作符,并得到了一個新的Optional
,然后,當我們繼續串聯lowercased()
的時候,卻直接使用了.
操作符,而沒有繼續使用swift?.uppercased()?.lowercased()
這樣的形式,這說明什么呢?
這也就是說,optional
在串聯
的時候,可以對前面方法返回的optional
進行unwrapping
,如果結果非nil
就繼續調用,否則就返回nil
。
但是……
這也有個特殊情況,就是如果調用的方法自身也返回一個optional
(注意:作為調用方法自身,是指的諸如uppercased()
這樣的方法,而不是整個swift?.uppercased()
表達式),那么你必須老老實實在每一個串聯的方法前面使用?.
操作符,來看下面這個例子。我們自己給String
添加一對toUppercased / toLowercased
方法,只不過,它們都返回一個String?
,當String為空字符串
時,它們返回nil
:
extension String {
func toUppercase() -> String? {
guard self.isEmpty != 0 else {
return nil
}
return self.uppercased()
}
func toLowercase() -> String? {
guard self.characters.count != 0 else {
return nil
}
return self.lowercased()
}
}
然后,還是之前optional chaining
的例子,這次,我們只能這樣寫:
let SWIFT1 = swift?.toUppercase()?.toLowercase()
注意到第二個?.
了么,由于前面的toUppercase()
返回了一個Optional
,我們只能用?.
來連接多個調用。而之前的uppercased()
則返回了一個String
,我們就可以直接使用.來串聯多個方法了。
除此之外,一種不太明顯的optional chaining
用法,就是用來訪問Dictionary中某個Value
的方法,因為[]操作符本身也是通過函數實現的,它既然返回一個optional
,我們當然也可以chaining
:
let numbers = ["fibo6": [0, 1, 1, 2, 3, 5]]
numbers["fibo6"]?[0] // 0
因此,絕大多數時候,如果你只需要在optional不為nil
時執行某些動作,optional chaining
可以讓你的代碼簡單的多,當然,如果你還了解了在chaining中執行的unwrapping
語義,就能在更多場景里,靈活的使用這個功能。
Nil coalescing
除了optional chaining
之外,Swift
還為optional
提供了另外一種語法上的便捷。如果我們希望在optional的值為nil
時設定一個默認值,該怎么做呢?可能你馬上就會想起Swift中的三元操作符:
var userInput: String? = nil
let username = userInput != nil ? userInput! : "Mars"
但就像你看到的,?:
操作符用在optional
上的時候顯得有些啰嗦,除此之外,為了實現同樣的邏輯,你還無法阻止一些開發者把默認的情況寫在:左邊:
let username = userInput == nil ? "Mars" : userInput!
如此一來,事情就不那么讓人開心了,當你穿梭在不同開發者編寫的代碼里,這種邏輯的轉換遲早會把你搞瘋掉。
于是,為了表意清晰的同時,避免上面這種順序上的隨意性,Swift引入了nil coalescing
,于是,之前username
的定義可以寫成這樣:
let username = userInput ?? "Mars"
其中,??
就叫做nil coalescing
操作符,optional
的值必須寫在左邊
,nil時的默認值必須寫在右邊
。這樣,就同時解決了美觀和一致性的問題。相比之前的用法,Swift再一次從語言設計層面履行了更容易用對,更不容易用錯的準則。
除了上面這種最基本的用法之外,??
也是可以串聯的,我們主要在下面這些場景里,串聯多個??
:
首先,當我們想找到多個optional
中,第一個不為nil的變量:
let a: String? = nil
let b: String? = nil
let c: String? = "C"
let theFirstNonNilString = a ?? b ?? c
// Optional("C")
在上面的例子里,我們沒有在表達式最右邊添加默認值。這在我們串聯多個??時是允許的,只不過,這樣的串聯結果,會導致theFirstNonNilString
的類型變成Optional
,當abc都為nil
時,整個表達式的值,就是nil
。
而如果我們這樣:
let theFirstNonNilString = a ?? b ?? "C"
theFirstNonNilString
的類型,就是String
了。理解了這個機制之后,我們就可以把它用在if
分支里,通過if let
綁定第一個不為nil
的optional
變量:
if let theFirstNonNilString = a ?? b ?? c {
print(theFirstNonNilString) // C
}
這樣的方式,要比你在if條件分支中,寫上一堆||直觀和美觀多了。
其次,當我們把一個雙層嵌套的optional
用在nil coalescing
操作符的串聯里時,要格外注意變量的評估順序。來看下面的例子:
假設,我們有三個optional
,第一個是雙層嵌套的optional
:
let one: Int?? = nil
let two: Int? = 2
let three: Int? = 3
當我們把one / two / three串聯起來時,整個表達式的結果是2。這個很好理解,因為,整個表達式中,第一個非nil的optional的值是2:
one ?? two ?? three // 2
當我們把one的值修改成.some(nil)時,上面這個表達式的結果是什么呢?
let one: Int?? = .some(nil)
let two: Int? = 2
let three: Int? = 3
one ?? two ?? three // nil
此時,這個表達式的結果會是nil,為什么呢?這是因為:
評估到one
時,它的值是.some(nil)
,但是.some(nil)并不是nil
,于是它自然就被當作第一個非nil的optional變量被采納了
;
被采納之后,Swift
會unwrapping
這個optional
的值作為整個表達式的值,于是就得到最終nil
的結果了;
理解了這個過程之后,我們再來看下面的表達式,它的值又是多少呢?
(one ?? two) ?? three // 3
正確的答案是3。這是因為我們要先評估()內的表達式,按照剛才我們提到的規則,(one ?? two)的結果是nil,于是nil ?? three的結果,自然就是3了。
當你完全理解了雙層嵌套的optional
在上面三個場景中的評估方式之后,你就明白為什么要對這種類型的串聯保持高度警惕了。因為,optional
的兩種值nil
和.some(nil)
,以及表達式中是否存在()改變優先級,都會影響整個表達式的評估結果。
為什么需要雙層嵌套的Optional
?
如果一個optional
封裝的類型又是一個optional
會怎樣呢?
首先,假設我們有一個String
類型的Array
:
let stringOnes: [String] = ["1", "One"]
當我們要把stringOnes
轉變成一個Int數組
的時候:
let intOnes = stringOnes.map { Int($0) }
此時,我們就會得到一個[Optional<Int>]
,當我們遍歷intOnes
的時候,就可以看到這個結果:
intOnes.forEach { print($0) }
// Optional<Int>
// nil
至此,一切都沒什么問題。但當你按照我們在之前提到過的while
的方式遍歷intOnes
的時候,你就會發現,Swift
悄悄對嵌套的optional
進行了處理:
var i = intOnes.makeIterator()
while let i = i.next() {
print(i)
}
// Optional<Int>
// nil
雖然,這會得到和之前for
循環同樣的結果。但是仔細分析while
的執行過程,你會發現,由于next()
自身返回一個optional
,而ineOnes
中元素的類型又是Optional<Int>
,因此intOnes
的迭代器指向的結果就是一個Optional<Optional<Int>>
。
當intOnes
中的元素不為nil
時,通過while let
得到的結果,就是我們看到的經過一層unwrapping之后的Optional(1)
;
當intOnes
中的元素為nil
時,我們可以看到while let
的到的結果并不是Optional(nil)
,而直接是nil
;
這說明Swift
對嵌套在optional
內部的nil
進行了識別,當遇到這類情況時,可以直接把nil
提取出來,表示結果為nil
。
了解了這個特性之后,我們就可以使用for...in
來正常遍歷intOnes了。例如,使用我們之前提到的for case
來讀取所有的非nil值:
for case let one? in intOnes {
print(one) // 1
}
或者統計所有的nil值:
for case nil in intOnes {
print("got a nil value")
}
如果Swift
不能對optional
中嵌套的nil
進行自動處理,上面的for
循環是無法正常工作的。
什么時候需要強制解包
我們都知道,對于一個optional
變量來說,可以用!來強行讀取optional
包含的值,Swift
管它叫作force unwrapping
。然而,這種操作并不安全
,強制讀取值為nil的optional會引發運行時錯誤
。于是,每當我們默默在一個optional后面寫上!的時候,心里總是會隱隱感到一絲糾結。我們到底什么時候該使用force unwrapping呢?
無論是在Apple的官方文檔,還是在Stack overflow上的各種討論中,你都能找到類似下面的言論:
永遠都不要使用這個東西,你會有更好的辦法;
當你確定optional一定不為nil時;
當你確定你真的必須這樣做時;
...
然而,當你沒有切身體會的時候,似乎很難理解這些言論的真實含義。其實,就在我們上一節內容的最后,就已經遇到了一個非常具體的例子:
extension Sequence {
func myFlatMap<T>(_ transform:
(Iterator.Element) -> T?) -> [T] {
return self.map(transform)
.filter { $0 != nil }
.map { $0! } // Safely force unwrapping
}
}
在我們用filter { $0 != nil }
過濾掉了self
中,所有的非nil
元素之后,在map
里,我們要獲得所有optional
元素中包含的值,這時,對$0
使用force unwrapping
,就滿足了之前提到的兩個條件:
我們可以確定此時$0一定不為nil
;
我們也確定真的必須如此;
現在,你對于“絕對安全”和“必須如此”這兩個條件,應該有一個更具體的認識了。所以,但凡沒有給你如此強烈安全感的場景,不要使用force unwrapping
。
而對于第一種“永遠都不要使用force unwrapping”
的言論,其實也有它的道理,畢竟在我們之前對optional
的各種應用方式里,你的確幾乎看不到我們使用了force unwrapping
。
甚至,即便當你身處在一個相當安全的環境里,的確相比force unwrapping
,你會有更好的方法。例如,對下面這個表示視頻信息的Dictionary來說:
let episodes = [
"The fail of sentinal values": 100,
"Common optional operation": 150,
"Nested optionals": 180,
"Map and flatMap": 220,
]
Key表示視頻的標題,Value表示視頻的秒數。
如果,我們要對視頻時長大于100秒的視頻標題排序,形成一個新的Array,就可以這樣:
episodes.keys
.filter { episodes[$0]! > 100 }
.sorted()
在filter
中,我們篩選大于100秒時長的視頻時,這里使用force unwrapping
也是絕對安全的。因為episode
是一個普通的Dictionary,它一定不為nil,因此,我們也一定可以使用keys讀到它的所有鍵值,即便episodes不包含任何內容也沒問題。然后,既然讀到了鍵值,用force unwrapping讀取它的value,自然也是安全的了
。
所以,這也算是一個可以使用force unwrapping
的場景。但就像我們剛才說的那樣,實際上,你仍有語義更好的表達方式,畢竟在filter內部再去訪問episodes看上去并不那么美觀。怎么做呢?
episodes.filter { (_, duration) in duration > 100 }
.map { (title, _) in title }
.sorted()
我們可以對整個Dictionary
進行篩選,首先找到所有時長大于100的視頻形成新的Dictionary
,然后,把所有的標題,map
成一個普通的Array
,最后,再對它排序。這樣,我們就不用任何force unwrapping
了,而且,就表意來說,要比之前的版本,容易理解的多。
兩個調試optional
的小技巧
盡管前面我們提到了很多使用optional
的正確方式,以及列舉了諸多不要使用force unwrapping
的理由,但現實中,你還是或多或少會跟各種使用了force unwrapping
的代碼打交道。使用這些代碼,就像拆彈一樣,稍不留神它就會讓我們的程序崩潰。因此,我們需要一些簡單易行的方式,讓它在跟我們翻臉前,至少留下些更有用的內容。
改進force unwrapping
的錯誤消息
得益于Swift可以自定義操作符的特性,一個更好的主意是我們自定義一個force unwrapping
操作符的加強版,允許我們自定義發生運行時錯誤的消息。既然一個!表示force unwrapping
,那我們暫且就定義一個!!操作符就好了。它用起來,像這樣:
var record = ["name": "11"]
record["type"] !! "Do not have a key named type"
怎么做呢?
首先,在上面的例子里,!!是一個中序操作符(infix operator)
,也就是說,它位于兩個操作數中間,我們這樣來定義它:
infix operator !!
其次,我們把它定義為一個泛型函數,因為我們并不知道optional
中包含的對象類型。這個函數有兩個參數,第一個參數是左操作數,表示我們要force unwrapping的optional
對象,第二個參數是右操作數
,表示我們要在訪問到nil時顯示的錯誤消息:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
// TODO: implement later
}
最后,!!<T>
的實現就很簡單了,成功unwrapping
到,就返回結果,否則,就用fatalError
打印運行時錯誤:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
if let value = optional { return value }
fatalError(errorMsg)
}
這樣,我們上面的record["type"]
就會得到下面的運行時錯誤:
fatal error
于是,即便發生意外,至少我們也還能夠讓程序“死個明白”。
進一步改進force unwrapping
的安全性
當然,除了在運行時死的明白之外,我們還可以把調試日志只留在debug mode
,并在release mode
,為force unwrapping到nil的情況
提供一個默認值。就像之前我們提到過的??
類似,我們來定義一個!?
操作符來實現這個過程:
infix operator !?
func !?<T: ExpressibleByStringLiteral>(
optional: T?,
errorMsg: @autoclosure () -> String) -> T {
assert(optional != nil, errorMsg())
return optional ?? ""
}
在上面的代碼里,我們使用ExpressibleByStringLiteral
這個protocol
約束了類型T必須是一個String
,之所以要做這個約束,是因為我們要為nil
的情況提供一個默認值。
在!?
的實現里,assert
僅在debug mode
生效,它的執行的邏輯,和我們實現!!
操作符時是一樣的。而在release mode
,我們直接使用了??
操作符,為String?
提供了一個空字符串默認值。
于是,當我們這樣使用record["type"]
的時候:
record["type"] !? "Do not have a key named type"
我們就只會在debug mode
得到和之前同樣的運行時錯誤,而在release mode
,則會得到一個空字符串。或者,基于這種方法,我們還可以有更靈活的選擇。例如,借助Tuple
,我們同時可以自定義nil時使用的默認值和運行時錯誤:
func !?<T: ExpressibleByStringLiteral>(
optional: T?,
nilDefault: @autoclosure () -> (errorMsg: String, value: T)) -> T {
assert(optional != nil, nilDefault().errorMsg)
return optional ?? nilDefault().value
}
然后,我們的record["Type"]
就可以改成:
record["type"] !? ("Do not have a key named type", "Free")
這樣,在release mode,record["type"]的值,就是“Free”了
。理解了這個方式的原理之后,我們就可以使用Swift
標準庫中提供了Expressible
家族,來對各種類型的optional
進行約束了:
ExpressibleByNilLiteral
ExpressibleByArrayLiteral
ExpressibleByFloatLiteral
ExpressibleByStringLiteral
ExpressibleByIntegerLiteral
ExpressibleByBooleanLiteral
...
最后,我們再來看一種特殊的情況,當我們通過optional chaining
得到的結果為Void?
時,例如這樣:
record["type"]?.write(" account")
由于Swift并沒有提供類似ExpressibleByVoidLiteral
這樣的protocol
,為了方便調試Optional<Void>
,我們只能再手動重載一個非泛型版本的!?
:
func !?(optional: Void?, errorMsg: @autoclosure () -> String) {
assert(optional != nil, errorMsg())
}
然后,就可以在debug mode
調試Optional<Void>
了:
record["type"]?
.write(" account")
!? "Do not have a key named type"