Swift中的Optional詳解

WechatIMG31.jpeg

對各種值為"空"的情況處理不當,幾乎是所有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,但調用tmprangeOfString方法卻是合法的,它會返回一個值為0的NSRange,因此,location的值也是0。

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

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

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你也無需自己實現這樣的findArray中自帶了一個index(of:)方法,它的功能和實現方式,和find是一樣的。

其次,在switch中使用optional 可選值類型?值的時候,我們也不用明確使用.some.noneSwift同樣做了類似的簡化:

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

因此,在Swiftfor循環里,每一次循環變量都是一個“新綁定”的結果,這樣,無論任何時間調用這個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

通常情況下,我們只能在optionalunwrapping的作用域內,來訪問它的值。
理解optional unwrapping的作用域

例如,在下面這個arrayProcess函數里:

func arrayProcess(array: [Int]) {
    if let first = array.first {
        print(first)
    }
}

我們只能在if代碼塊內部,訪問被unwrapping之后的值。但這樣做有一個麻煩,就是如果我們要在函數內部的多個地方使用array.first,就要在每個地方都進行某種形式的unwrapping,這不僅寫起來很麻煩,還會讓代碼看上去非常凌亂。

實際上,面對這種在多處訪問同一個optional的情況,更多的時候,我們需要的是一個確保optional一定不為nil的環境。如果,我們能在一個地方統一處理optioanlnil的情況,就可以在這個地方之外,安全的訪問optional的值了。

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

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

    print(first)
}

在上面的例子里,我們使用guard let綁定了array.first的非nil值。如果array.firstnil,就會轉而執行else代碼塊里的內容。這樣,我們就可以在else內部,統一處理array.firstnil的情況。在這里,我們可以編寫任意多行語句,唯一的要求,就是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。只要前一個方法返回optional類型,我們就可以一直把調用串聯下去。但是,如果你仔細觀察上面的串聯方法,卻可以發現一個有趣的細節:對于第一個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綁定第一個不為niloptional變量:

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變量被采納了
被采納之后,Swiftunwrapping這個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"
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容

  • 基礎部分(The Basics) 當推斷浮點數的類型時,Swift 總是會選擇Double而不是Float。 結合...
    gamper閱讀 1,317評論 0 7
  • Swift 是一門開發 iOS, macOS, watchOS 和 tvOS 應用的新語言。然而,如果你有 C 或...
    XLsn0w閱讀 929評論 2 1
  • 簡介 這是一個Swift語言教程,基于最新的iOS 9,Xcode 7.3和Swift 2.2,會為你介紹Swif...
    春泥Fu閱讀 573評論 0 0
  • 時光荏苒,光陰似箭。轉眼間我已經快要踏入大學生活一年了,從最開始的各種新奇,到現在的成熟。我真的很高興自己在這么...
    南方少年w閱讀 250評論 0 1