第四章——可選型別技術之旅

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

可選繫結

我們可以利用if let語法進行可選繫結:

if let idx = array.indexOf("four") {
array.removeAtIndex(idx)
}
複製程式碼

這裡的let表示變數idx不可被修改,如果它需要修改,可以用var,但是這個變數是原來變數的拷貝,對它的修改不會影響到原來的變數。

可選繫結的if可以有where從句。比如可以這樣移除陣列的第一個元素:

if let idx = array.indexOf("four")
where idx != array.startIndex {
array.removeAtIndex(idx)
}
複製程式碼

可選繫結可以用多個從句,後面的變數依賴於前面的可選繫結成功進行,否則整個if語句就會終止:

let string = "http://www.google.com/images/srpr/logo11w.png"
if let url = NSURL(string: string),
data = NSData(contentsOfURL: url),
image = NSImage(data: data)
{
let view = UIImageView(image: image)
XCPShowView("Download image", view: view)
}
複製程式碼

多個變數的每個部分都可以有一個where從句:

if let url = NSURL(string: string) where url.pathExtension == "png" ,
data = NSData(contentsOfURL: url), image = NSImage(data: data)
{
let view = UIImageView(image: image)
}
複製程式碼

我們可以在在if語句中進行可選繫結,也可以在while迴圈中進行可選繫結。這表示一個迴圈,這個迴圈只能在返回nil時結束:

let array = [1,2,3]
var generator = array.generate()
while let i = generator.next() {
print(i)
}
複製程式碼

雙層巢狀可選型別

可選型別包裝的值當然可以還是一個可選型別,這就是可選型別的巢狀。假設有一個字串陣列,我們把陣列中的字串轉換成數字,可以用map方法實現:

let stringNumbers = ["1", "2", "3", "foo"]
let maybeInts = stringNumbers.map{ Int($0) }
複製程式碼

由於Int.init(String)是可失敗構造器,maybeInts中的元素型別是Int?。因為foo不是整數,所以陣列的最後一個元素是nil。我們可以嘗試在while let迴圈中解封最外層的可選型別,看看其中包含的是不是nil,如果不是的話就先解封,然後執行迴圈的主體部分:

var generator = mabeInts.generate()
while let maybeInt = generator.next() {
//maybeInt的型別是Int?
}
複製程式碼

當迴圈到最後一個元素——根據"foo"字串得到的nil,時next()方法返回的是.Some(nil)。然後這個返回結果被解封,其中包含的的nilmaybeInt繫結。如果我們只想遍歷陣列中所有不為nil的元素,在for迴圈中可以用for case匹配:

for case let i? in maybeInts {
//i是Int型別而不是Int?
}
複製程式碼

我們用到了i?這種寫法,這表示只匹配不為nil的值。這是.Some(i)的縮寫,所以剛剛那個迴圈也可以這樣寫:

for case let .Some(i) in maybeInts {
}
複製程式碼

關於使用case匹配的原理和更多用法,我的另一篇文章從原理分析Swift的switch怎麼比較物件中有詳細解釋

未解封的可選型別的作用域

先來看一段程式碼:

if let firstElement = a.first {
//使用firstElement
}

//在if程式碼塊外,你無法使用firstElement
複製程式碼

這裡的firstElement,只能在if語句中使用。事實上這樣不僅不會導致不方便,反而有好處。我們“不得不”解封並使用a.first,這確保我們不會因為粗心而忘記相應的檢查。

如果我們只關心可選繫結失敗了會怎樣,那麼就可以使用guard let

func doStuffWithFileExtension(fileName: String) {
guard let period = fileName.characters.indexOf(".")
else { return }
let extensionRange = period.successor()..<fileName.endIndex
let fileExtension = fileName[extensionRange]
print(fileExtension)
}
複製程式碼

注意,我們必須保證在else的結尾,能跳出當前的作用域,一般可以通過returnfatalError實現。如果在迴圈中使用guard,那麼在else中應該用breakcontinue

我們可以注意到guardif有些類似。有些時候用guardif更復雜。不過guard在閱讀程式碼時也是一個醒目的標記,它表示“我在做一些檢查,而且檢查不通過就會退出”。編譯器會確保你真的會在堅持不通過時退出當前程式碼塊,否則就會產生編譯錯誤。所以如果使用guardif皆可,更推薦使用guard

可選鏈

在Objective-C中,向nil物件傳送訊息其實是空指令。在Swift中,通過可選繫結可以達到同樣的效果:

self.delegate?.callback()
複製程式碼

通過可選鏈呼叫的方法返回的結果也是可選型別。比如:

//假設我們有一個Int?型別的變數i,我們想要找到它的successor
let j = i?.successor()
複製程式碼

正如可選鏈的名字所示,它可以把多個操作串聯起來:

let j = i?.successor().successor()
複製程式碼

不過這看上去有些奇怪。我們剛剛說過可選鏈的返回結果還是可選鏈,所以在第一個successor後面為什麼沒有?呢?

其實官方文件已經說的很清楚了,可選鏈是在每個可選型別的後面加上?。這裡的i是可選型別,所以呼叫它的successor()需要加?,但successor()函式本來的返回結果是Int,所以不用加?。這裡需要嚴格區分一個概念,即函式的返回型別和可選鏈的返回型別。比如successor()函式返回結果是Int但整個可選鏈返回的結果是Int?,這是由於可選鏈有可能因為inil而失敗,問題並不是出在successor()函式上。

換句話說,如果successor()函式自身的返回結果是可選型別的,那我們就需要在後面加上?表示把這個可選鏈延續下去。

可選連結串列示一種可選型別的串聯,所以在下標指令碼和呼叫函式時也可以使用可選鏈。它不僅作為表示式的右值,還可以作為左值:

splitViewController?.delegate = myDelegate
複製程式碼

空合運算子

如果想在解封可選型別遇到nil時,用一個預設值替換它,這時候可以使用空合運算子:

let stringInteger = "1"
let i = Int(stringInteger) ?? 0
複製程式碼

空合運算子和?:三目運算子有些類似,事實上空合運算子總是可以用三目運算子重寫,不過程式碼會比較複雜。我們可以在獲取陣列的第一個元素是,提供一個預設值:

let i = array.first ?? 0
複製程式碼

這樣的程式碼更加簡潔明瞭,一眼就能看出目的在於獲取陣列中第一個元素,新增在??後面的0是預設值。此前的三元運算子,首先要檢查,然後才是返回值,最後是預設值。這個檢查的語法還很容易寫反(不小心把預設值放在中間,真實值放在最後)。

空合運算子還可以形成一個鏈。如果我們有多個變數可能是可選型別,並希望選擇第一個非可選型別的變數,可以這樣寫:

let i: Int? = nil
let j: Int? = nil
let k: Int? = 42

let n = i ?? j ?? k ?? 0
複製程式碼

空合運算子也可以不提供預設值,但這樣最後的返回結果是可選型別的:

let m = i ?? j ?? k
// m 的型別是Int?
複製程式碼

遇到巢狀的可選型別,需要注意區別a ?? b ?? c(a ?? b) ?? c。前者是空合運算子串聯,得到的依然是可選型別,而後者首先解封內層,再解封外層:

let s1: String?? = nil
(s1 ?? "inner") ?? "outer"  //值為inner
let s2: String?? = .Some(nil)
(s2 ?? "inner") ?? "outer"  //值為outer
複製程式碼

可選型別map

我們經常會遇到這種情況:”接收一個可選型別,並且如果它不是nil就對他進行某種變換“

這時就可以用可選型別的map方法,它和陣列的map方法非常類似。它會接受一個閉包,表示了可選型別中的值的變換邏輯:

var i: Int? = 1
let j = i.map{ 2 * $0}
複製程式碼

如果inil,map方法不做任何處理,所以j也是nil。像這裡i的值為1,map方法就對i中包裝的元素(1)進行變換。這裡的j的值就是Optional(2)

可選型別的map可以這樣實現:

extension Optional {
func map<U>(transform: Wrapped -> U) -> U? {
if let value = self {
return transform(value)
}
return nil
}
}
複製程式碼

map用途很多,比如如們想過載reduce方法,它不接受初始值,而是直接把集合中的第一個元素當做初始值:

var array: Array<Int> = [1,2,3,4]
array.reduce(+)
複製程式碼

由於執行reduce方法的陣列可能為空,所以這個方法的返回值必須是可選型別(如果沒有初始值,也就不會得到返回值,只能返回nil)。這就是本節開始我們提到的那個常用模式。所以可以使用map方法來實現reduce方法:

extension Array {
func reduce(combine: (Element, Element) -> Element) -> Element? {
return first.map {
self.dropFirst().reduce($0, combine: combine)
}
}
}
複製程式碼

可選型別flatMap

在可選型別的map方法中,如果變換函式的返回值也是可選型別,那麼返回結果就是巢狀的可選型別。比如獲取陣列的第一個字串,並轉化為整數:

let x = stringNumbers.first.map { Int($0) }
複製程式碼

因為map返回的是可選型別,而Int(String)返回的本來就是可選型別,所以x的型別是Int??。這時候我們可以用flatMap方法把返回結果變成一個單層的可選型別:

let y = stringNumbers.first.flatMap { Int($0) }
複製程式碼

這時候的返回值y就是Int?型別的了。

或者也可以用if let語句來寫,因為可以根據先繫結的值計算出後繫結的值:

if let a = stringNumbers.first, b = Int(a) {
print(b)
}
複製程式碼

所以flatMap可以這樣實現:

extension Optional {
func flatMap<U>(transform: Wrapped -> U?) -> U? {
if let value = self, let transformed = transform(value) {
return transformed
}
return nil
}
}
複製程式碼

通過flatMap過濾nil

對於一個可選型別變數的集合,我們可能希望忽略掉其中的nil

考慮一個實際問題,有一個陣列,裡面有若各個字串,我們希望求出所有字串轉化成數字之和的和。如果某個字串不能轉化成數字,就不考慮它。

實際上我們只需要一個可以過濾掉nilmap方法即可。標準庫中sequence過載的faltMap方法就是這麼做的,所以我們可以這樣實現:

let numbers = ["1", "2", "3", "foo"]
numbers.flatMap { Int($0) }.reduce(0, combine: +)  // 結果為6
複製程式碼

如果我們要自己實現這個flatMap方法,我們先要定義一個flatten1方法,過濾掉陣列中所有的nil,返回由所有不為nil的可選型別構成的陣列:

func flatten1<S: SequenceType, T where S.Generator.Element == T?>(source: S) -> [T] {
return Array(source.lazy.filter { $0 != nil}.map { $0! })
}
複製程式碼

這個方法沒有擴充任何協議,因為我們要確保呼叫這個方法的Sequence中的元素必須是可選型別,而協議擴充並不支援這一點。不過有了這個方法之後,我們要實現的flatMap方法就很簡單了:

extension SequenceType {
func flatMap<U>(transform: Generator.Element -> U?) -> [U] {
return flatten1(self.lazy.map(transform))
}
}
複製程式碼

在上面兩個方法中,lazy修飾符延遲真正的陣列建立,可以避免為臨時的中間陣列分配記憶體空間。雖然這是一個比較小的優化,但如過陣列很大,這麼做還是值得的。

可選型別的相等

有時候我們不關注可選型別是否為nil,而關注它是否含有某個確切的值:

if regex.characters.first == "^" {
//只匹配字串的開始
}
複製程式碼

要想這麼寫,我們需要讓可選型別實現==運算子:

func ==<T: Equatable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case (nil, nil): return true
case let (x?, y?): return x == y
case (_?, nil), (nil, _?): return false
}
}
複製程式碼

不過根據過載的==運算子,我們的程式碼應該這樣寫:

//要比較兩個可選型別,其實不需要把"^"宣告成可選型別。
if regex.characters.first == Optional("^") {
//只匹配字串的開始
}
複製程式碼

之所以我們不用寫.Some(),是因為swift總是在需要的時候可以把非可選型別升級成可選型別以完成型別匹配。這種自動升級非常重要,比如我們實現可選型別的map方法時,變換了可選型別內部的值並返回,但map的返回結果是可選型別的,所以其實是編譯器自動為我們做了這一步的轉化。這樣就不用把程式碼寫成:Optional(transform(value))了。

Swift的程式碼也依賴於這一特性。比如字典的下標指令碼根據鍵來查詢值,並返回它的可選型別。它的下標指令碼既可以獲取值,也可以賦值,如果沒有隱式轉化,賦值就要這樣寫:myDict["someKey"] = Optional(someValue)

基於鍵的下標指令碼賦值,如果傳入的值為nil,那麼這個鍵會被移除。這個特性很有用,但在用字典時,遇到可選型別也要小心:

var dictWithNils: [String: Int?] = [
"one": 1,
"two": 2,
"none": nil,
]
複製程式碼

這個字典有三個鍵,其中一個的值是nil。如果我們想讓"two"的值也是nil,這樣的程式碼是錯誤的:

dictWithNils["two"] = nil
複製程式碼

因為它實際上會把"two"這個鍵移除。

如果我們想改變某個鍵的值,需要在下面的三種寫法中任選一種自己覺得比較清晰的:

dictWithNils["two"] = Optional(nil)
dictWithNils["two"] = .Some(nil)
dictWithNils["two"]? = nil
複製程式碼

第三種寫法和前兩種的區別在於,它是基於可選鏈的,所以如果鍵不存在,它不會插入一條新的記錄。

回到主題上來,儘管可選型別過載了==運算子,但這並不表示他們就能實現Equatable協議。根據可選型別過載的==運算子可以看出,這需要保證任意一個它所包含的值的型別都實現了==運算子。一旦可選型別實現了Equatable協議,就可以在case語句中進行匹配。

關於使用case匹配的原理和更多用法,我的另一篇文章從原理分析Swift的switch怎麼比較物件中有詳細解釋

比較可選型別

==運算子類似,我們還可以實現可選型別的<運算子,這需要可選型別內部封裝的值實現了Comparable協議。nil總是比任何非nil的值小。這就意味著,nil任何負數都小。這一點在排序時需要重視:

let temps = ["-459.67", "98.6", "0", "warm"]
print(temps.sort { Double($0) < Double($1) })
複製程式碼

從執行結果中可以看出,warm轉換成數字是小於-459.67的

["warm", "-459.67", "0", "98.6"]
複製程式碼

相關文章