對各種值為"空"的情況處理不當(dāng)殴俱,幾乎是所有Bug的來源养交。
在其它編程語言里,空值的表達方式多種多樣:"" / nil / NULL / 0 / nullptr 都是我們似曾相識的表達空值的方法枕荞。
而當(dāng)我們訪問一個變量時柜候,我們有太多情況無法意識到一個變量有可能為空,進而最終在程序中埋藏了一個個閃退的隱患躏精。
因此渣刷,Swift里,明確區(qū)分了"變量"和"值有可能為空的變量"這兩種情況矗烛,以時刻警告你:"哦辅柴,它的值有可能為空,我應(yīng)該謹慎處理它瞭吃。
而對于后者碌嘀,謹慎不僅僅是精神層面的,Swift還從語法層面上歪架,幫助你在處理空值時股冗,游刃有余。
NSString *tmp = nil;
if ([tmp rangeOfString: @"Swift"].location != NSNotFound) {
// Will print out for nil string
NSLog(@"Something about swift");
}
在我們的例子里牡拇,盡管tmp
的值是nil
魁瞪,但調(diào)用tmp
的rangeOfString
方法卻是合法的,它會返回一個值為0的NSRange
惠呼,因此导俘,location
的值也是0。
但是剔蹋,NSNotFound
的值卻是NSIntegerMax
旅薄。于是,盡管tmp
的值為ni
l,我們還可以在控制臺看到_Something about swift_
這樣的輸出少梁。
那么Swift
中是怎么解決的呢洛口?
Swift的方法,通過把不同的結(jié)果放在一個enum里凯沪,
Swift可以通過編譯器第焰,強制我們明確處理函數(shù)返回的異常情況。
Optional關(guān)鍵實現(xiàn)技術(shù)模擬
讓編譯器強制我們處理可能發(fā)生錯誤的情況妨马。為了做到這點挺举,我們得滿足下面這幾個條件:
1、 首先烘跺,作為一個函數(shù)的返回值湘纵,它仍舊得是一個獨立的類型;
2滤淳、 其次梧喷,對于所有成功的情況,這個類型得有辦法包含正確的結(jié)果脖咐;
3铺敌、 最后,對于所有錯誤的情況文搂,這個類型得有辦法用一個和正確情況類型不同的值來表達适刀;
4、 做到這些煤蹭,當(dāng)我們把一個錯誤情況的值用在正常的業(yè)務(wù)邏輯之后笔喉,編譯器就可以由于類型錯誤,給我們予以警告了硝皂。
讓編譯器強制你處理錯誤的情況
說到這常挚,我們應(yīng)該就有思路了,一個包含兩個case的enum正是解決這個問題的完美方案:
enum Optional<T> {
case some(T) //對于所有成功的情況稽物,我們用case some奄毡,并且把成功的結(jié)果保存在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
的實現(xiàn)里咪奖,它有兩個退出函數(shù)的路徑盗忱。當(dāng)在Array
中找到參數(shù)時,就把對應(yīng)的Index
作為.some
的associated value
并返回羊赵;否則趟佃,當(dāng)while
循環(huán)結(jié)束時,就返回.none
。這樣闲昭,當(dāng)我們用find
查找元素位置時:
var numbers = [1, 2, 3]
let index = numbers.find(4)
print(type(of: index)) // Optinal<Int>
index
的類型就會變成Optional<Int>
罐寨,于是,當(dāng)我們嘗試把這個類型傳遞給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")
}
看到了么鸯绿?只要會發(fā)生錯誤的函數(shù)返回Optional
,編譯器就會強制我們對調(diào)用成功和失敗的情況明確分開處理簸淀。并且楞慈,當(dāng)你看到一個函數(shù)返回了Optional
,從它的簽名就可以知道啃擦,Hmmm,調(diào)用它有可能會發(fā)生錯誤饿悬,我得小心處理令蛉。
實際上,你并不需要自己定義這樣的Optional
類型狡恬,Swift
中的optional
變量就是如此實現(xiàn)的珠叔,因此酌摇,當(dāng)讓find
直接返回一個Index optional
時:
func find(_ element: Element) -> Index? {
// ...
}
理解Swift
對optional
類型進行的簡化處理
Optional
作為Swift
中最重要的語言特性之一枯芬,為了避免讓你每次都通過.some
和.none
來處理不同的情況(畢竟珍剑,這是optional
的實現(xiàn)細節(jié))媒抠,Swift
在語法層面對這個類型做了諸多改進轴合。
首先半夷,optional
包含的.some
關(guān)聯(lián)值會在必要的時候耘沼,被自動升級成optional
碌冶;而nil
字面值則會被轉(zhuǎn)換成.none
庸追。因此霍骄,我們之前的find
可以被實現(xiàn)成這樣:
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你也無需自己實現(xiàn)這樣的find
读整,Array
中自帶了一個index(of:)
方法,它的功能和實現(xiàn)方式咱娶,和find
是一樣的米间。
其次,在switch
中使用optional 可選值類型膘侮?
值的時候屈糊,我們也不用明確使用.some
和.none
,Swift
同樣做了類似的簡化:
switch index {
case let index?:
numbers.remove(at: index)
case nil:
print("Not exist")
}
我們可以用case let index
?這樣的形式來簡化讀取.some
的關(guān)聯(lián)值喻喳,用case nil
來簡化case .none
另玖。
有哪些常用的optional
使用范式
if let
如果我們要表達“當(dāng)optional
不等于nil
時,則執(zhí)行某些操作”這樣的語義,最樸素的寫法谦去,是這樣的:
let number: Int? = 1
if number != nil {
print(number!)
}
其中慷丽,number!
這樣的寫法叫做force unwrapping
,用于強行讀取optional
變量中的值鳄哭,此時要糊,如果optional
的值為nil
就會觸發(fā)運行時錯誤。所以妆丘,通常锄俄,我們會事先判斷optional
的值是否為nil
。
但這樣寫有一個弊端勺拣,如果我們需要在if代碼塊中包含多個訪問number
的語句奶赠,就要在每一處使用number!
,這顯得很啰嗦药有。我們明知此時number
的值不為nil
毅戈,應(yīng)該可以直接使用它的值才對。為此愤惰,Swift
提供了if let
的方式苇经,像這樣:
if let number = number {
print(number)
}
在上面的代碼里,我們使用if let
直接在if
代碼塊內(nèi)部宦言,定義了一個新的變量number
扇单,它的值是之前number?
的值。然后奠旺,我們就可以在if
代碼塊內(nèi)部蜘澜,直接通過新定義的number來訪問之前number?
的值了。
這里用了一個小技巧凉倚,就是在if let
后面新定義變量的名字兼都,和之前的optional
是一樣的。這不僅讓代碼看上去就像是訪問optional
自身一樣稽寒,而且扮碧,通常為一個optional
的值另取一個新的名字,也著實沒什么必要杏糙。
除了可以直接在if let
中綁定optional
的value
慎王,我們還可以通過布爾表達式進一步約束optional
的值,這也是一個常見的用法宏侍,例如赖淤,我們希望number為奇數(shù):
if let number = number, number % 2 != 0 {
print(number)
}
我們之前講到過逗號操作符在if
中的用法,在這里谅河,number % 2 != 0
中的number
咱旱,指的是在if
代碼塊中新定義的變量确丢,理解了這點,上面的代碼就不存在任何問題了吐限。
有了optional
的這種用法之后鲜侥,對于那些需要一連串有可能失敗的行為都成功時才執(zhí)行的動作,只要這些行為都返回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
對象,到根據(jù)url
創(chuàng)建Data
肌蜻,到用data
創(chuàng)建一個UIImage
互墓,每一步的繼續(xù)都依賴于前一步的成功,而每一步調(diào)用的方法又都返回一個optional
蒋搜,因此轰豆,通過串聯(lián)多個if let,我們就把每一步成功的結(jié)果綁定在了一個新的變量上并傳遞給下一步齿诞,這樣,比我們在每一步不斷的去判斷optional
是否為nil
簡單多了骂租。
while let
除了在條件分支中使用let
綁定optional
祷杈,我們也可以在循環(huán)中,使用類似的形式渗饮。例如但汞,為了遍歷一個數(shù)組,我們可以這樣:
let numbers = [1, 2, 3, 4, 5, 6]
var iterator = numbers.makeIterator()
while let element = iterator.next() {
print(element)
}
在這里互站,iterator.next()
會返回一個Optional<Int>
私蕾,直到數(shù)組的最后一個元素遍歷完之后,會返回nil
胡桃。然后踩叭,我們用while let
綁定了數(shù)組中的每一個值,并把它們打印在了控制臺上翠胰。
看到這里容贝,你可能會想,直接用一個for...in...
數(shù)組不就好了么之景?為什么要使用這種看上去有點兒麻煩的while
呢斤富?
實際上,通過這個例子锻狗,我們要說明一個重要的問題:在Swift里满力,for...in
循環(huán)是通過while
模擬出來的焕参,這也就意味著,for
循環(huán)中的循環(huán)變量在每次迭代的時候油额,都是一個全新的對象叠纷,而不是對上一個循環(huán)變量的修改:
for element in numbers {
print(element)
}
在上面這個for...in
循環(huán)里,每一次迭代悔耘,element
都是一個全新的對象讲岁,而不是在循環(huán)開始創(chuàng)建了一個element
之后,不斷去修改它的值衬以。用while
的例子去理解缓艳,每一次for
循環(huán)迭代中的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
調(diào)用澈蚌,你期望會返回什么結(jié)果呢?我們在每一次for...in
循環(huán)中灼狰,定義了一個打印循環(huán)變量i的箭頭函數(shù)宛瞄。當(dāng)它們執(zhí)行的時候,也許你會不假思索的脫口而出:當(dāng)然是輸出0, 1, 2啊交胚。
但實際上份汗,由于循環(huán)變量i自始至終都是同一個變量,在最后調(diào)用fnArray
中保存的每一個函數(shù)時蝴簇,它們在真正執(zhí)行時訪問的杯活,也都是同一個變量i。因此熬词,這三個調(diào)用打印出來的值旁钧,都是2。類似這樣的問題互拾,稍不注意均践,就會在代碼中,埋下Bug的隱患摩幔。
因此彤委,在Swift
的for
循環(huán)里,每一次循環(huán)變量都是一個“新綁定”的結(jié)果或衡,這樣焦影,無論任何時間調(diào)用這個clousre
车遂,都不會出現(xiàn)類似JavaScript中的問題了。
我們把之前的那個例子斯辰,用Swift
重寫一下:
var fnArray: [()->()] = []
for i in 0...2 {
fnArray.append({ print(i) })
}
fnArray[0]() // 0
fnArray[1]() // 1
fnArray[2]() // 2
這里舶担,由于變量i在每次循環(huán)都是一個新綁定的結(jié)果,因此彬呻,每一次添加到fnArray
中的clousre
捕獲到的變量都是不同的對象衣陶。當(dāng)我們分別調(diào)用它們的時候,就可以得到捕獲到它們的時候闸氮,各自的值了剪况。
使用guard
簡化optional unwrapping
通常情況下,我們只能在optional
被unwrapping
的作用域內(nèi)蒲跨,來訪問它的值译断。
理解optional unwrapping
的作用域
例如,在下面這個arrayProcess
函數(shù)里:
func arrayProcess(array: [Int]) {
if let first = array.first {
print(first)
}
}
我們只能在if
代碼塊內(nèi)部或悲,訪問被unwrapping
之后的值孙咪。但這樣做有一個麻煩,就是如果我們要在函數(shù)內(nèi)部的多個地方使用array.first
巡语,就要在每個地方都進行某種形式的unwrapping
翎蹈,這不僅寫起來很麻煩,還會讓代碼看上去非常凌亂男公。
實際上杨蛋,面對這種在多處訪問同一個optional
的情況,更多的時候理澎,我們需要的是一個確保optional
一定不為nil
的環(huán)境。如果曙寡,我們能在一個地方統(tǒng)一處理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
蕊唐,就會轉(zhuǎn)而執(zhí)行else
代碼塊里的內(nèi)容屋摔。這樣,我們就可以在else
內(nèi)部替梨,統(tǒng)一處理array.first
為nil
的情況钓试。在這里装黑,我們可以編寫任意多行語句,唯一的要求弓熏,就是else
的最后一行必須離開當(dāng)前作用域恋谭,對于函數(shù)來說,就是從函數(shù)返回挽鞠,或者調(diào)用fatalError
表示一個運行時錯誤疚颊。
而這,也是為數(shù)不多的信认,我們可以在value binding
作用域外部材义,來訪問optional value
的情況。
一個特殊情況
在Swift
里狮杨,有一類特殊的函數(shù)母截,它們返回Never
,表示這類方法直到程序執(zhí)行結(jié)束都不會返回橄教。Swift
管這種類型叫做uninhabited type
清寇。
什么情況會使用Never
呢?其實并不多护蝶,一種是崩潰前华烟,例如,使用fatalError
返回一些用于排錯的消息持灰;另一種盔夜,是類似dispatchMain
這樣,在進程生命周期中一直需要執(zhí)行的方法堤魁。
當(dāng)我們在返回Never
的函數(shù)中喂链,使用guard
時,else
語句并不需要離開當(dāng)前作用域妥泉,而是最后一行必須調(diào)用另外一個返回Never
的函數(shù)就好了椭微。例如下面的例子:
func toDo(item: String?) -> Never {
guard let item = item else {
fatalError("Nothing to do")
}
fatalError("Implement \(item) later")
}
在toDo
的實現(xiàn)里,如果我們沒有指定要完成的內(nèi)容盲链,就在else
里調(diào)用fatalError
顯示一個錯誤蝇率。在這里,fatalError
也是一個返回Never
的函數(shù)刽沾。
一個偽裝的optional
除了使用真正的optional
變量之外本慕,有時,我們還是利用編譯器對optional
的識別機制來為變量的訪問創(chuàng)造一個安全的使用環(huán)境侧漓。例如锅尘,為了把數(shù)組中第一個元素轉(zhuǎn)換為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
纺弊;
其次牛欢,從程序的執(zhí)行路徑分析,對于firstNumber
來說淆游,要不我們已經(jīng)在if let
中完成了初始化傍睹;要不,我們已經(jīng)從else
返回犹菱。因此拾稳,只要程序的執(zhí)行邏輯來到了if...else...
之后,訪問firstNumber
就一定是安全的了腊脱。
實際上访得,Swift
編譯器也可以識別這樣的執(zhí)行邏輯。firstNumber
就像一個偽裝的optional
一樣陕凹,在if let
分支里被初始化成具體的值悍抑,在else
分支里,被認為值是nil
杜耙。因此搜骡,在else
代碼塊之后,就像在之前guard
語句之后一樣佑女,我們也可以認為firstNumber
一定是包含值的记靡,因此安全的訪問它。
通常团驱,當(dāng)我們要調(diào)用一個包含在optional
中的對象的方法時摸吠,我們可能會像下面這樣把兩種情況分開處理:
var swift: String? = "Swift"
let SWIFT: String
if let swift = swift {
SWIFT = swift.uppercased()
}
else {
fatalError("Cannot uppercase a nil")
}
但是,當(dāng)我們僅僅想獲得一個包含結(jié)果的optional類型時嚎花,上面的寫法就顯得有點兒啰嗦了寸痢。實際上,我們有更簡單的用法:
let SWIFT = swift?.uppercased() // Optional("SWIFT")
這樣贩幻,我們就會得到一個新的Optional
。并且两嘴,我們還可以把optional
對象的方法調(diào)用串聯(lián)起來:
let SWIFT = swift?.uppercased().lowercased()
// Optional("swift")
上面的形式丛楚,在Swift
里,就叫做optional chaining
憔辫。只要前一個方法返回optiona
l類型趣些,我們就可以一直把調(diào)用串聯(lián)下去。但是贰您,如果你仔細觀察上面的串聯(lián)方法坏平,卻可以發(fā)現(xiàn)一個有趣的細節(jié):對于第一個optional
拢操,我們調(diào)用uppercased()
方法使用的是?.
操作符,并得到了一個新的Optional
舶替,然后令境,當(dāng)我們繼續(xù)串聯(lián)lowercased()
的時候,卻直接使用了.
操作符顾瞪,而沒有繼續(xù)使用swift?.uppercased()?.lowercased()
這樣的形式舔庶,這說明什么呢?
這也就是說陈醒,optional
在串聯(lián)
的時候惕橙,可以對前面方法返回的optional
進行unwrapping
,如果結(jié)果非nil
就繼續(xù)調(diào)用钉跷,否則就返回nil
弥鹦。
但是……
這也有個特殊情況,就是如果調(diào)用的方法自身也返回一個optional
(注意:作為調(diào)用方法自身爷辙,是指的諸如uppercased()
這樣的方法彬坏,而不是整個swift?.uppercased()
表達式),那么你必須老老實實在每一個串聯(lián)的方法前面使用?.
操作符犬钢,來看下面這個例子苍鲜。我們自己給String
添加一對toUppercased / toLowercased
方法,只不過玷犹,它們都返回一個String?
混滔,當(dāng)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
领跛,我們只能用?.
來連接多個調(diào)用。而之前的uppercased()
則返回了一個String
撤奸,我們就可以直接使用.來串聯(lián)多個方法了吠昭。
除此之外,一種不太明顯的optional chaining
用法胧瓜,就是用來訪問Dictionary中某個Value
的方法矢棚,因為[]操作符本身也是通過函數(shù)實現(xiàn)的,它既然返回一個optional
府喳,我們當(dāng)然也可以chaining
:
let numbers = ["fibo6": [0, 1, 1, 2, 3, 5]]
numbers["fibo6"]?[0] // 0
因此蒲肋,絕大多數(shù)時候,如果你只需要在optional不為nil
時執(zhí)行某些動作,optional chaining
可以讓你的代碼簡單的多兜粘,當(dāng)然申窘,如果你還了解了在chaining中執(zhí)行的unwrapping
語義,就能在更多場景里孔轴,靈活的使用這個功能剃法。
Nil coalescing
除了optional chaining
之外,Swift
還為optional
提供了另外一種語法上的便捷距糖。如果我們希望在optional的值為nil
時設(shè)定一個默認值玄窝,該怎么做呢?可能你馬上就會想起Swift中的三元操作符:
var userInput: String? = nil
let username = userInput != nil ? userInput! : "Mars"
但就像你看到的悍引,?:
操作符用在optional
上的時候顯得有些啰嗦恩脂,除此之外,為了實現(xiàn)同樣的邏輯趣斤,你還無法阻止一些開發(fā)者把默認的情況寫在:左邊:
let username = userInput == nil ? "Mars" : userInput!
如此一來俩块,事情就不那么讓人開心了,當(dāng)你穿梭在不同開發(fā)者編寫的代碼里浓领,這種邏輯的轉(zhuǎn)換遲早會把你搞瘋掉玉凯。
于是,為了表意清晰的同時联贩,避免上面這種順序上的隨意性漫仆,Swift引入了nil coalescing
,于是泪幌,之前username
的定義可以寫成這樣:
let username = userInput ?? "Mars"
其中盲厌,??
就叫做nil coalescing
操作符,optional
的值必須寫在左邊
祸泪,nil時的默認值必須寫在右邊
吗浩。這樣,就同時解決了美觀和一致性的問題没隘。相比之前的用法懂扼,Swift再一次從語言設(shè)計層面履行了更容易用對,更不容易用錯的準則右蒲。
除了上面這種最基本的用法之外阀湿,??
也是可以串聯(lián)的,我們主要在下面這些場景里瑰妄,串聯(lián)多個??
:
首先陷嘴,當(dāng)我們想找到多個optional
中,第一個不為nil的變量:
let a: String? = nil
let b: String? = nil
let c: String? = "C"
let theFirstNonNilString = a ?? b ?? c
// Optional("C")
在上面的例子里翰撑,我們沒有在表達式最右邊添加默認值罩旋。這在我們串聯(lián)多個??時是允許的,只不過眶诈,這樣的串聯(lián)結(jié)果涨醋,會導(dǎo)致theFirstNonNilString
的類型變成Optional
,當(dāng)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條件分支中,寫上一堆||直觀和美觀多了尽棕。
其次喳挑,當(dāng)我們把一個雙層嵌套的optional
用在nil coalescing
操作符的串聯(lián)里時,要格外注意變量的評估順序滔悉。來看下面的例子:
假設(shè)伊诵,我們有三個optional
,第一個是雙層嵌套的optional
:
let one: Int?? = nil
let two: Int? = 2
let three: Int? = 3
當(dāng)我們把one / two / three串聯(lián)起來時回官,整個表達式的結(jié)果是2曹宴。這個很好理解,因為歉提,整個表達式中笛坦,第一個非nil的optional的值是2:
one ?? two ?? three // 2
當(dāng)我們把one的值修改成.some(nil)時,上面這個表達式的結(jié)果是什么呢唯袄?
let one: Int?? = .some(nil)
let two: Int? = 2
let three: Int? = 3
one ?? two ?? three // nil
此時弯屈,這個表達式的結(jié)果會是nil,為什么呢恋拷?這是因為:
評估到one
時资厉,它的值是.some(nil)
,但是.some(nil)并不是nil
蔬顾,于是它自然就被當(dāng)作第一個非nil的optional變量被采納了
宴偿;
被采納之后,Swift
會unwrapping
這個optional
的值作為整個表達式的值诀豁,于是就得到最終nil
的結(jié)果了窄刘;
理解了這個過程之后,我們再來看下面的表達式舷胜,它的值又是多少呢娩践?
(one ?? two) ?? three // 3
正確的答案是3。這是因為我們要先評估()內(nèi)的表達式,按照剛才我們提到的規(guī)則翻伺,(one ?? two)的結(jié)果是nil材泄,于是nil ?? three的結(jié)果,自然就是3了吨岭。
當(dāng)你完全理解了雙層嵌套的optional
在上面三個場景中的評估方式之后拉宗,你就明白為什么要對這種類型的串聯(lián)保持高度警惕了。因為辣辫,optional
的兩種值nil
和.some(nil)
旦事,以及表達式中是否存在()改變優(yōu)先級,都會影響整個表達式的評估結(jié)果急灭。
為什么需要雙層嵌套的Optional
?
如果一個optional
封裝的類型又是一個optional
會怎樣呢姐浮?
首先,假設(shè)我們有一個String
類型的Array
:
let stringOnes: [String] = ["1", "One"]
當(dāng)我們要把stringOnes
轉(zhuǎn)變成一個Int數(shù)組
的時候:
let intOnes = stringOnes.map { Int($0) }
此時葬馋,我們就會得到一個[Optional<Int>]
单料,當(dāng)我們遍歷intOnes
的時候,就可以看到這個結(jié)果:
intOnes.forEach { print($0) }
// Optional<Int>
// nil
至此点楼,一切都沒什么問題扫尖。但當(dāng)你按照我們在之前提到過的while
的方式遍歷intOnes
的時候,你就會發(fā)現(xiàn)掠廓,Swift
悄悄對嵌套的optional
進行了處理:
var i = intOnes.makeIterator()
while let i = i.next() {
print(i)
}
// Optional<Int>
// nil
雖然换怖,這會得到和之前for
循環(huán)同樣的結(jié)果。但是仔細分析while
的執(zhí)行過程蟀瞧,你會發(fā)現(xiàn)沉颂,由于next()
自身返回一個optional
,而ineOnes
中元素的類型又是Optional<Int>
悦污,因此intOnes
的迭代器指向的結(jié)果就是一個Optional<Optional<Int>>
铸屉。
當(dāng)intOnes
中的元素不為nil
時,通過while let
得到的結(jié)果切端,就是我們看到的經(jīng)過一層unwrapping之后的Optional(1)
彻坛;
當(dāng)intOnes
中的元素為nil
時,我們可以看到while let
的到的結(jié)果并不是Optional(nil)
踏枣,而直接是nil
昌屉;
這說明Swift
對嵌套在optional
內(nèi)部的nil
進行了識別,當(dāng)遇到這類情況時茵瀑,可以直接把nil
提取出來间驮,表示結(jié)果為nil
。
了解了這個特性之后,我們就可以使用for...in
來正常遍歷intOnes了。例如,使用我們之前提到的for case
來讀取所有的非nil值:
for case let one? in intOnes {
print(one) // 1
}
或者統(tǒng)計所有的nil值:
for case nil in intOnes {
print("got a nil value")
}
如果Swift
不能對optional
中嵌套的nil
進行自動處理涛菠,上面的for
循環(huán)是無法正常工作的屹篓。
什么時候需要強制解包
我們都知道煮嫌,對于一個optional
變量來說,可以用!來強行讀取optional
包含的值抱虐,Swift
管它叫作force unwrapping
。然而饥脑,這種操作并不安全
恳邀,強制讀取值為nil的optional會引發(fā)運行時錯誤
。于是灶轰,每當(dāng)我們默默在一個optional后面寫上!的時候谣沸,心里總是會隱隱感到一絲糾結(jié)。我們到底什么時候該使用force unwrapping呢笋颤?
無論是在Apple的官方文檔乳附,還是在Stack overflow上的各種討論中,你都能找到類似下面的言論:
永遠都不要使用這個東西伴澄,你會有更好的辦法赋除;
當(dāng)你確定optional一定不為nil時;
當(dāng)你確定你真的必須這樣做時非凌;
...
然而举农,當(dāng)你沒有切身體會的時候,似乎很難理解這些言論的真實含義敞嗡。其實颁糟,就在我們上一節(jié)內(nèi)容的最后,就已經(jīng)遇到了一個非常具體的例子:
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
勺像;
我們也確定真的必須如此起惕;
現(xiàn)在,你對于“絕對安全”和“必須如此”這兩個條件咏删,應(yīng)該有一個更具體的認識了惹想。所以,但凡沒有給你如此強烈安全感的場景督函,不要使用force unwrapping
嘀粱。
而對于第一種“永遠都不要使用force unwrapping”
的言論激挪,其實也有它的道理,畢竟在我們之前對optional
的各種應(yīng)用方式里锋叨,你的確幾乎看不到我們使用了force unwrapping
垄分。
甚至,即便當(dāng)你身處在一個相當(dāng)安全的環(huán)境里娃磺,的確相比force unwrapping
薄湿,你會有更好的方法。例如偷卧,對下面這個表示視頻信息的Dictionary來說:
let episodes = [
"The fail of sentinal values": 100,
"Common optional operation": 150,
"Nested optionals": 180,
"Map and flatMap": 220,
]
Key表示視頻的標(biāo)題豺瘤,Value表示視頻的秒數(shù)。
如果听诸,我們要對視頻時長大于100秒的視頻標(biāo)題排序坐求,形成一個新的Array,就可以這樣:
episodes.keys
.filter { episodes[$0]! > 100 }
.sorted()
在filter
中晌梨,我們篩選大于100秒時長的視頻時桥嗤,這里使用force unwrapping
也是絕對安全的。因為episode
是一個普通的Dictionary仔蝌,它一定不為nil泛领,因此,我們也一定可以使用keys讀到它的所有鍵值敛惊,即便episodes不包含任何內(nèi)容也沒問題师逸。然后,既然讀到了鍵值豆混,用force unwrapping讀取它的value篓像,自然也是安全的了
。
所以皿伺,這也算是一個可以使用force unwrapping
的場景员辩。但就像我們剛才說的那樣,實際上鸵鸥,你仍有語義更好的表達方式奠滑,畢竟在filter內(nèi)部再去訪問episodes看上去并不那么美觀。怎么做呢妒穴?
episodes.filter { (_, duration) in duration > 100 }
.map { (title, _) in title }
.sorted()
我們可以對整個Dictionary
進行篩選宋税,首先找到所有時長大于100的視頻形成新的Dictionary
,然后讼油,把所有的標(biāo)題杰赛,map
成一個普通的Array
,最后矮台,再對它排序乏屯。這樣根时,我們就不用任何force unwrapping
了,而且辰晕,就表意來說蛤迎,要比之前的版本,容易理解的多含友。
兩個調(diào)試optional
的小技巧
盡管前面我們提到了很多使用optional
的正確方式替裆,以及列舉了諸多不要使用force unwrapping
的理由,但現(xiàn)實中窘问,你還是或多或少會跟各種使用了force unwrapping
的代碼打交道辆童。使用這些代碼,就像拆彈一樣南缓,稍不留神它就會讓我們的程序崩潰。因此荧呐,我們需要一些簡單易行的方式汉形,讓它在跟我們翻臉前,至少留下些更有用的內(nèi)容倍阐。
改進force unwrapping
的錯誤消息
得益于Swift可以自定義操作符的特性概疆,一個更好的主意是我們自定義一個force unwrapping
操作符的加強版,允許我們自定義發(fā)生運行時錯誤的消息峰搪。既然一個!表示force unwrapping
岔冀,那我們暫且就定義一個!!操作符就好了。它用起來概耻,像這樣:
var record = ["name": "11"]
record["type"] !! "Do not have a key named type"
怎么做呢使套?
首先,在上面的例子里鞠柄,!!是一個中序操作符(infix operator)
侦高,也就是說,它位于兩個操作數(shù)中間厌杜,我們這樣來定義它:
infix operator !!
其次奉呛,我們把它定義為一個泛型函數(shù),因為我們并不知道optional
中包含的對象類型夯尽。這個函數(shù)有兩個參數(shù)瞧壮,第一個參數(shù)是左操作數(shù),表示我們要force unwrapping的optional
對象匙握,第二個參數(shù)是右操作數(shù)
咆槽,表示我們要在訪問到nil時顯示的錯誤消息:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
// TODO: implement later
}
最后,!!<T>
的實現(xiàn)就很簡單了圈纺,成功unwrapping
到罗晕,就返回結(jié)果济欢,否則,就用fatalError
打印運行時錯誤:
func !!<T>(optional: T?,
errorMsg: @autoclosure () -> String) -> T {
if let value = optional { return value }
fatalError(errorMsg)
}
這樣小渊,我們上面的record["type"]
就會得到下面的運行時錯誤:
fatal error
于是法褥,即便發(fā)生意外,至少我們也還能夠讓程序“死個明白”酬屉。
進一步改進force unwrapping
的安全性
當(dāng)然半等,除了在運行時死的明白之外,我們還可以把調(diào)試日志只留在debug mode
呐萨,并在release mode
杀饵,為force unwrapping到nil的情況
提供一個默認值。就像之前我們提到過的??
類似谬擦,我們來定義一個!?
操作符來實現(xiàn)這個過程:
infix operator !?
func !?<T: ExpressibleByStringLiteral>(
optional: T?,
errorMsg: @autoclosure () -> String) -> T {
assert(optional != nil, errorMsg())
return optional ?? ""
}
在上面的代碼里切距,我們使用ExpressibleByStringLiteral
這個protocol
約束了類型T必須是一個String
,之所以要做這個約束惨远,是因為我們要為nil
的情況提供一個默認值谜悟。
在!?
的實現(xiàn)里,assert
僅在debug mode
生效北秽,它的執(zhí)行的邏輯葡幸,和我們實現(xiàn)!!
操作符時是一樣的。而在release mode
贺氓,我們直接使用了??
操作符蔚叨,為String?
提供了一個空字符串默認值。
于是辙培,當(dāng)我們這樣使用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
標(biāo)準庫中提供了Expressible
家族羔飞,來對各種類型的optional
進行約束了:
ExpressibleByNilLiteral
ExpressibleByArrayLiteral
ExpressibleByFloatLiteral
ExpressibleByStringLiteral
ExpressibleByIntegerLiteral
ExpressibleByBooleanLiteral
...
最后肺樟,我們再來看一種特殊的情況,當(dāng)我們通過optional chaining
得到的結(jié)果為Void?
時逻淌,例如這樣:
record["type"]?.write(" account")
由于Swift并沒有提供類似ExpressibleByVoidLiteral
這樣的protocol
么伯,為了方便調(diào)試Optional<Void>
,我們只能再手動重載一個非泛型版本的!?
:
func !?(optional: Void?, errorMsg: @autoclosure () -> String) {
assert(optional != nil, errorMsg())
}
然后卡儒,就可以在debug mode
調(diào)試Optional<Void>
了:
record["type"]?
.write(" account")
!? "Do not have a key named type"