Swift中的Optional詳解

WechatIMG31.jpeg

對各種值為"空"的情況處理不當(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)用tmprangeOfString方法卻是合法的,它會返回一個值為0的NSRange惠呼,因此导俘,location的值也是0。

但是剔蹋,NSNotFound的值卻是NSIntegerMax旅薄。于是,盡管tmp的值為nil,我們還可以在控制臺看到_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作為.someassociated 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? {
    // ...
}

理解Swiftoptional類型進行的簡化處理

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.noneSwift同樣做了類似的簡化:

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中綁定optionalvalue慎王,我們還可以通過布爾表達式進一步約束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的隱患摩幔。

因此彤委,在Swiftfor循環(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

通常情況下,我們只能在optionalunwrapping的作用域內(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)一處理optioanlnil的情況糠爬,就可以在這個地方之外,安全的訪問optional的值了举庶。

好在执隧,Swift在語法上,對這個操作進行了支持户侥,這就是guard的用法:

func arrayProcess(array: [Int]) {
    guard let first = array.first else {
        return
    }

    print(first)
}

在上面的例子里镀琉,我們使用guard let綁定了array.first的非nil值。如果array.firstnil蕊唐,就會轉(zhuǎn)而執(zhí)行else代碼塊里的內(nèi)容屋摔。這樣,我們就可以在else內(nèi)部替梨,統(tǒng)一處理array.firstnil的情況钓试。在這里装黑,我們可以編寫任意多行語句,唯一的要求弓熏,就是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憔辫。只要前一個方法返回optional類型趣些,我們就可以一直把調(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綁定第一個不為niloptional變量:

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變量被采納了宴偿;
被采納之后,Swiftunwrapping這個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"
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末田柔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骨望,更是在濱河造成了極大的恐慌硬爆,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擎鸠,死亡現(xiàn)場離奇詭異缀磕,居然都是意外死亡,警方通過查閱死者的電腦和手機劣光,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門袜蚕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赎线,你說我怎么就攤上這事廷没『ィ” “怎么了垂寥?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長另锋。 經(jīng)常有香客問我滞项,道長,這世上最難降的妖魔是什么夭坪? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任文判,我火速辦了婚禮,結(jié)果婚禮上室梅,老公的妹妹穿的比我還像新娘戏仓。我一直安慰自己,他們只是感情好亡鼠,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布赏殃。 她就那樣靜靜地躺著,像睡著了一般间涵。 火紅的嫁衣襯著肌膚如雪仁热。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天勾哩,我揣著相機與錄音抗蠢,去河邊找鬼举哟。 笑死,一個胖子當(dāng)著我的面吹牛迅矛,可吹牛的內(nèi)容都是我干的妨猩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼诬乞,長吁一口氣:“原來是場噩夢啊……” “哼册赛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起震嫉,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤森瘪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后票堵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扼睬,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年悴势,在試婚紗的時候發(fā)現(xiàn)自己被綠了窗宇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡特纤,死狀恐怖军俊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捧存,我是刑警寧澤粪躬,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站昔穴,受9級特大地震影響镰官,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吗货,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一泳唠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宙搬,春花似錦笨腥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窥摄,卻和暖如春镶奉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工哨苛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸽凶,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓建峭,卻偏偏與公主長得像玻侥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亿蒸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 基礎(chǔ)部分(The Basics) 當(dāng)推斷浮點數(shù)的類型時凑兰,Swift 總是會選擇Double而不是Float。 結(jié)合...
    gamper閱讀 1,264評論 0 7
  • Swift 是一門開發(fā) iOS, macOS, watchOS 和 tvOS 應(yīng)用的新語言边锁。然而姑食,如果你有 C 或...
    XLsn0w閱讀 920評論 2 1
  • 簡介 這是一個Swift語言教程,基于最新的iOS 9茅坛,Xcode 7.3和Swift 2.2音半,會為你介紹Swif...
    春泥Fu閱讀 560評論 0 0
  • 時光荏苒,光陰似箭贡蓖。轉(zhuǎn)眼間我已經(jīng)快要踏入大學(xué)生活一年了曹鸠,從最開始的各種新奇,到現(xiàn)在的成熟斥铺。我真的很高興自己在這么...
    南方少年w閱讀 240評論 0 1