iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
0

可選鏈 (Optional Chaining)

可選鏈是一個查詢和調用可能目前為 nil 的可選屬性、方法和下標過程。如果一個可選包含一個值,那麼屬性、方法跟下標調用將會成功。如果可選為 nil ,那麼屬性、方法跟下標調用將返回 nil 。多個查詢可以鏈在一起,如果鏈中的任何一個節點為 nil ,那麼整個鏈就會失敗。


可選鏈代替強制展開

你可以透過在調用可選項不是 nil 就調用屬性、方法或者腳本的可選值後邊使用問號(?)來明確可選鏈。這與在可選值之後放置驚嘆號(!)以強制解包相似。主要的區別在於可選鏈在可選項為 nil 時會失敗,而強制展開則在可選項為 nil 時觸發運行時錯誤。

為了顯示出可選鏈可以在 nil 值上調用,可選鏈調用的結果一定是一個可選值,就算你查詢的屬性、方法或者下標返回的是非可選值。你可以使用這個可選項返回值來檢查可選鏈調用成功(返回的可選項有值),還是由於鏈中出現了 nil 而導致沒有成功(返回的可選值為 nil )。

另外,可選鏈調用的結果與期望的返回值類型相同,只是包裝成了可選項。像是,返回 Int 的屬性通過可選鏈後會返回一個 Int?。

首先我們先創建兩個 Class , 一個為 Student 和 English ,English 實例有一個 Int 屬性叫做 engScore ,它帶有默認值 60 , Student 實例有一個 English? 類型的可選 score 屬性:

class Student {
    var score:English?
}

class English {
    var engScore = 60
}

我們創建一個新的 Student 實例,基於可選項的特性,它的 score 會默認初始化為 nil 。所以創建的 Jeremy 的 sroce 值為 nil:

var Jeremy = Student()

https://ithelp.ithome.com.tw/upload/images/20180112/201077017dgSZ2DXEn.png

如果你這時使用驚嘆號(!)去強制解包,則會運行錯誤,因為沒有值可以展開:
https://ithelp.ithome.com.tw/upload/images/20180112/20107701N0PkBMVOIl.png

因此我們可以使用 if 方式來判斷其中是否有值,如果其中有則輸出結果,無值則印出 else 的資訊:

if let engTest = Jeremy.score?.engScore {
    print("Jeremy的英文分數為\(engTest)")
} else {
    print("Jeremy這次英文缺考")
}

這將會告訴Swift把可選 score 屬性“鏈接”起來並且取回 engScore 的值,如果 score 存在的話。

https://ithelp.ithome.com.tw/upload/images/20180112/20107701EIg93tQKKL.png

由於嘗試訪問 english 有失敗的可能,可選鏈嘗試返回一個 Int? 類型的值(可選的Int)。當 score 為 nil ,就如同上面的範例,這個可選 Int 將也會是 nil ,來反應出不能訪問 engTest 的問題。可選的 Int 透過可選綁定來展開整數並賦值非可選值給 engTest 。就算 engScore 是非可選的 Int 也是可以的。事實上通過可選鏈查詢就意味著對 engScore 的調用一定會返回 Int? 而不是 Int 。

所以我們這邊將 English 實例賦值給 Jeremy.score ,那麼那就會有默認值 60 :

Jeremy.score = English()

所以我們在執行上面那段程式碼,就會順利輸出他的英文成績:

https://ithelp.ithome.com.tw/upload/images/20180112/20107701CemXoCUbIv.png


為可選鏈定義模型類

你可以使用可選鏈來調用屬性、方法和下標不止一個層級。這允許你在相關類型的複雜模型中深入到子屬性,並檢查是否可以在這些自屬性裡訪問屬性、方法和下標。

交易這個 class 與上述範例差不多,只有一個可選類型為 Store? 的 store 變數:

// 交易
class Trade {
    var store:Store?
}

商店這個 class 中我們有一個儲存 ShoppingList 實例的數組, numberOfList 則是來儲存數組中 .count 的值,並且我們創建一個下標可以讀寫的來訪問 list 數組的索引位置,作為這個數組賦值的快捷方式,並提供一個方法印出清單數量,最後定義了一個可選類型 customer,類型為 Customer?。

// 商店
class Store {
    var list = [ShoppingList]()
    var numberOfList:Int {
        return list.count
    }
    subscript(i: Int) -> ShoppingList {
        get {
            return list[i]
        }
        set {
            list[i] = newValue
        }
    }
    func checkList() {
        print("購物清單目前有:\(numberOfList)項物品")
    }
    var customer:Customer?
}

上面變數 list 數組使用的 ShoppingList 類型設置一個名為 name 的屬性,提供一個初始化器來給這個屬性設置物品名稱:

// 購物清單
class ShoppingList {
    let name:String
    init(name:String) {
        self.name = name
    }
}

最後一個創建一個客戶資料的類型 。這個類型有兩個 String ? ,這兩個屬性, name 和 phone ,是定義客戶的姓名以及電話,並提供一個 checkData 的方法,用來輸出客戶資料,裡面有一些判斷式,如果符合要求就印出客戶資訊,如果沒輸入資料或是沒有輸入名稱就輸出一個 "必須輸入用戶姓名資訊" :

// 客戶資料
class Customer {
    var name:String?
    var phone:String?
    
    func checkData() -> String {
        if name != nil , phone == nil{
            return "用戶名稱為:\(name!),尚未輸入聯絡電話"
        } else if phone != nil && name != nil {
            return "用戶名稱為:\(name!),聯絡電話為:\(phone!)"
        } else {
            return "必須輸入用戶姓名"
        }
    }
}

通過可選鏈訪問屬性

你可以使用可選鏈來訪問可選值裡的屬性,檢查這個屬性的訪問是否成功。使用上邊定義的類來創建一個新的 Trade 實例,並訪問它的 numberOfList 屬性:

let trade = Trade()

if let numberOfList = trade.store?.numberOfList {
    print("交易清單一有購買\(numberOfList)項物品")
} else {
    print("購物清單無物品")
}

但是結果如上面的範例相同,因為 trade.store 為 nil , 所以這個調用失敗了。

https://ithelp.ithome.com.tw/upload/images/20180112/20107701K8zwaPjXZ1.png

我們嘗試透過可選鏈來給屬性賦值:

let someOne = Customer()
someOne.name = "Jeremy"
someOne.phone = "0911234555"
trade.store?.customer = someOne

給 trade.store 的 customer 屬性賦值依然會失敗,因為 trade.store 目前是 nil 。

這個賦值是可選鏈的一部分,也就是說 = 運算符右手側的程式碼都不會被評判。在先前的範例中,不容易看出 someOne 沒有被評判,因為賦值給一個常量不會有任何副作用。

下面我們使用一個函數來創建客戶做同樣的賦值,但我們在函數中加上 "建立客戶資料" 這條訊息,這可以讓你清楚的看到 = 運算符右手側是否被評判。

func createCustomer() -> Customer {
    print("建立客戶資料.")
    
    let customer = Customer()
    customer.name = "Jeremy"
    customer.phone = "0911000222"
    trade.store?.customer = customer
    
    return customer
}

john.residence?.address = createAddress() 

我們可以看到它並沒有調用 createCustomer() 這個函數,因為沒有訊息被輸出。

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180112/201077010r8GvZ8Xef.png


通過可選鏈調用方法

你可以使用可選鏈來調用可選項裡的方法,並且檢查調用是否成功。你甚至可以在沒有定義返回值的方法上這麼做。

舉我們 Store 這個 class 中的函數:

func checkList() {
        print("購物清單目前有:\(numberOfList)項物品")
    }

這個方法沒有指定返回類型,函數和方法如果沒有返回類型就隱式地指明為 Void 類型。意思是說它們返回一個 () 的值或者是一個空的元組。

如果你用可選鏈在可選項裡調用這個方法,方法的返回類型將會是 Void? ,而不是 Void,因為當你通過可選鏈調用的時候返回值一定會是一個可選類型。這允許你使用 if 語句來檢查是否能調用 checkList() 方法,就算是方法自身沒有定義返回值也可以。通過對比調用 checkList 返回的值是否為 nil 來確定方法的調用是否成功:

if trade.store?.checkList() != nil {
    print("可以調用此方法")
} else {
    print("無法調用此方法")
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180112/2010770141Jjd0VTSY.png

如果你嘗試通過可選鏈來設置屬性也是一樣的。上邊通過可選鏈訪問屬性中的例子嘗試設置 customer 值給 trade.store ,就算是 store 屬性是 nil 也行。任何通過可選鏈設置屬性的嘗試都會返回一個 Void? 類型值,它允許你與 nil 比較來檢查屬性是否設置成功:

if (trade.store?.customer = someOne) != nil {
    print("可以成功賦值給 customer ")
} else {
    print("無法賦值給 customer")
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180112/20107701XjbMVv3lGJ.png


通過可選鏈訪問下標

你同樣可以使用可選鏈來給可選項下標訪問或設置值,並且檢查下標的調用是否成功。

我們使用下標訪問 trade.store 屬性的數組 list 裡第一個項目名稱,由於 trade.store 目前是 nil,所以下標調用也會失敗:

if let firstItem = trade.store?.list[0].name {
    print("list 中,第一個項目為\(firstItem)")
} else {
    print("無法查看 list 第一個項目")
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180112/20107701FZkzcJuwTW.png

同樣的,你可以嘗試在可選鏈裡用下標來設置一個新值,但也還是一樣會無法設置:

trade.store?[0] = ShoppingList(name: "書籍")

如果你創建並且賦值一個實際的 Store 實例給trade.store ,在 list 數組裡添加一個或者多個 ShoppingList 實例,你就可以通過可選鏈使用 Store 下標來訪問 list 數組裡的實際元素了:

let AStore = Store()
AStore.list.append(ShoppingList(name: "書籍"))
AStore.list.append(ShoppingList(name: "漫畫"))
trade.store = AStore

這時候我們再來查看 list 中的第一個項目,結果如下:
https://ithelp.ithome.com.tw/upload/images/20180112/20107701bPFwOknsGg.png


訪問可選類型的下標

如果下標返回是一個可選類型的值,像是 Swift的字典類型的鍵下標——放一個問號在下標的方括號後邊來鏈接它的可選返回值,首先我們先創建一個英文成績,然後來更改字典內的內容:

var engScore = ["Jeremy":[82,98,99],"Allan":[45,67,88] ]
engScore["Jeremy"]?[2] += 1
engScore["Allan"]?[0] = 87
engScore["Curry"]?[0] = 23

修改的第一行是將 Jeremy 中第三個元素加 1 ,所以他的值會變成 100;第二行則是將他第一項的值改為 87;第三行因為沒有 Curry 這個鍵,所以調用失敗


上一篇
Day-23 Swift 語法(19) - 自動引用計數 (ARC)
下一篇
Day-25 Swift 語法(21) - 鏈的多層連接
系列文
Swift 菜鳥的30天30

尚未有邦友留言

立即登入留言