Swift 程式語言 — Optional Chaining

讓我們來看看 Optional Chaining 到底做了些什麼吧!

Jeremy Xue
Jeremy Xue ‘s Blog
15 min readOct 20, 2019

--

Photo by Mael BALLAND on Unsplash

前言:

Optional chaining 是一個查詢或調用當前可能為 nil 的 optional 屬性、方法和下標的過程。如果 optional 包含一個值,則屬性、方法或下標調用成功;如果 optional 為 nil,則屬性、方法或下標調用返回 nil。可以將多個查詢鏈結在一起,如果鏈中的任何鏈接為 nil,則整個鏈都算是失敗。

|Optional Chaining 作為強制展開的替代方法

你透過在要調用的屬性、方法和下標後放置問號(?)來指定 optional chaining,如果 optaional 的值不為 nil。這與在 optional 值之後放置驚嘆號(!)來強制展開值非常相似。主要差別是,當 optional 值為 nil 時,Optaion chaining 會優雅的失敗,而強制展開則會觸發 Runtime error。

為了反應可以在 nil 值上調用 optional chaining 的事實,即使你要查詢的屬性、方法和下標返回的是 non-optional 值,Optional chaining 調用的結果也始終是 Optional 值。你可以使用這個 optional 的返回值來檢查 optional chaining 調用是否成功(返回的 optional 包含一個值),或者由於鏈中的值為 nil 而失敗(返回的 optional 為 nil)。

具體來說,Optional chaining 調用的結果與預期返回值的類型相同,但是包裝在 optional 中。當訪問 optional chaining 時,通常返回 Int 的屬性將返回 Int?

接下來的幾段程式碼演示了 optional chaining 與強制展開哪裡不同,並使你能夠檢查是否成功。

首先,我們定義了兩個 class,分別是 PersonResidence

Residence 實例具有一個名為 numberOfRoomsInt 屬性,默認值為 1Person 實例具有類型為 Residence? 的 optional Residence 屬性。

如果你創建一個新的 Person 實例,其 residence 屬性由於是可選屬性而默認初始化為 nil

在下面的程式碼中,johnresidence 屬性值為 nil

如果你嘗試訪問這個 johnresidencenumberOfRooms 屬性,方法是在 residence 後放置一個感嘆號(!)來強制展開其值,而觸發 Runtime error,因為沒有 residence 的值可以展開:

上面的程式碼在 john.residence 具有非 nil 值時成功,並將 roomCount 設置為包含適當房間數的 Int 值。但是,如上所述,當 residencenil,此程式碼始終會觸發 Runtime error。

Optional chaining 提供一種訪問 numberOfRooms 值的替代方法。要使用 optional chaining,請使用問號(?)代替驚嘆號(!):

這告訴 Swift 去鏈結在 optional 的 residence 屬性,如果在 residence 存在時的情況下檢索 numberOfRooms 的值。

由於嘗試訪問 numberOfRooms 可能會失敗,因此 optional chaining 嘗試返回 Int? 類型的值或 optional Int。如上面範例所示,當 residencenil 時,這個 optional Int 也將為 nil 來反映無法訪問 numberOfRooms 的事實。透過 optional binding 訪問 optional Int 可以展開整數並將 non-optional 的值分配給 roomCount 變數。

請注意,即使 numberOfRooms 是 non-optional 的 Int 也是如此。透過 optional chaining 查詢它的事實意味著 numberOfRooms 的調用始終返回 Int? 而不是 Int

你可以將 Residence 實例分配給 john.residence,使其不再具有 nil 值:

john.residence 現在包含一個實際的 Residence 實例,而不是 nil。如果你嘗試與使用之前相同的 optional chaining 來訪問 numberOfRooms,現在他將返回一個 Int? 包含默認的 numberOfRooms1

|定義 Optional Chaining 的 Model Classes

你可以使用 optional chaining 與深度超過一層的屬性、方法和下標中一起使用。這使你可以深入到相互關聯類型的複雜模型中的子屬性,並且檢查是否可以訪問這些子屬性上的屬性、方法和下標。

下面的程式碼片段定義了四個 modal class 用於後續的幾個範例中,包含多層級 optional chaining 的範例。這些 classes 透過帶有相關屬性、方法和下標的 RoomAddress class,擴展了上面的 PersonResidence model。

Person class 的定義方式與以前相同:

Residence class 比之前複雜許多。這次,Residence class 定義了一個名為 rooms 的變數屬性,該屬性使用 [Room] 類型的空陣列初始化:

因為這個版本的 Residence 儲存了一個陣列的 Room 實例,所以其 numberOfRooms 屬性被實現為計算屬性,而非儲存屬性。計算出 numberOfRooms 屬性只是從 rooms 陣列中返回 count 屬性的值。

作為一個訪問其 rooms 陣列的捷徑,這個版本的 Residence 提供了一個可讀寫的下標,這個下標提供了對 rooms 陣列中 index 的訪問。

這個版本的 Residence 還提供了一個名為 printNumberOfRooms 的方法,該方法只是單純印出該住宅的房間數量。

最後,Residence 定義了一個名為 address,其類型為 Address?。這個 Address 類型在下面定義

用於 rooms 陣列的 Room 是一個簡單的 class,具有一個名為 name 的屬性,以及一個用於將屬性設置為合適的房間名稱的初始化器:

在這個 model 中的最後一個 class 稱為 Address。這個 class 具有三個 optional 的屬性其類型為 String?。前面兩個屬性 buildingNamebuildingNumber 是將特定建築物標誌為地址一部分的替代方法。第三個屬性 street 用來為該地址街道命名。

Address class 還提供了一個名為 buildingIdentifier 的方法,該方法返回類型 String?。此方法檢查地址的屬性,如果 buildingName 有值,則返回 buildingName,如果 buildingNumberstreet 皆有值,則返回兩者串聯值,否則返回 nil

|通過 Optional Chaining 訪問屬性

如同上面 〔Optional Chaining 作為強制展開的替代方法〕所述,可以使用 optional chaining 訪問 optional 值上的屬性,並檢查該屬性訪問是否成功。

使用上面定義的 classes 創建一個新的 Person 實例,然後嘗試像以前一樣訪問其 numberOfRooms 屬性:

由於 john.residencenil,因此 optional chaining 的調用與之前一樣失敗了。

你還可以嘗試通過 optional chaining 設置屬性的值:

這個範例中,嘗試設置 john.residenceaddress 屬性將會失敗,因為 john.residence 目前為 nil

賦值是 optional chaining 的一部分,這意味著 = 運算符號右側的任何程式碼都不會被評估。在上一個範例中,不容易看到 somAddress 從未被評估,因為訪問常數沒有任何副作用。下面的範例執行相同的賦值,但是它是使用一個函數來創建地址。該函數在返回值之前印出了 “Fuction was Called”,這使你可以查看 = 運算符右側的程式碼是否被評估。

你可以說 createAddress 函數沒有被調用,因為沒有印出任何內容。

|通過 Optional chaining 調用方法

你可以使用 optional chaining 來對 optional 值調用方法,並檢查該方法是否調用成功。你可以執行此操作,即使該方法沒有定義回傳值。

Residence 中的 printNumberOfRooms 方法將印出 numberOfRooms 的當前值。該方法看起來如下:

這個方法沒有指定返回類型。但是,沒有返回類型的函數和方法的隱式返回類型為 Void,如無返回值的函數中所述。這意味著它們的返回值為 () 或一個空的元組。

如果你透過 optional chaining 對 optional 值調用這個方法,則該方法的返回類型將為 Void?,而不是 Void。因為通過 optional chaining 調用時,返回值始終為 optional 類型。這使你能夠使用 if 語法來檢查是否可以調用 printNumberOfRooms 方法,即使該方法本身沒有定義返回值。將 printNumberOfRooms 調用的返回值與 nil 進行比較,來查看該方法是否調用成功:

如果你嘗試通過 optional chaining 設置屬性時,也是如此。上面的〔通過 Optional Chaining 訪問屬性〕中的範例嘗試為 john.residence 設置地址值,即使 Residence 屬性為 nil。任何通過 optional chaining 設置屬性的嘗試都會返回 Void 類型的值,這使你可以與 nil 進行比較來查看是否成功設置了該屬性:

|通過 Optional Chaining 訪問下標

你可以使用 optional chaining 來嘗試從下標中檢索和設置 optional 的值,並檢查該下標調用是否成功。

下面的範例嘗試使用 Residence 定義的下標檢索 john.residence 屬性的 rooms 陣列中第一個房間的名稱。因為 john.residence 當前為 nil,所以下標調用失敗:

這個下標調用中的 optional chaining 的問號放在 john.residence 之後,並在下標括號之前。因為 john.residence 是 optional 嘗試進行 optional chaining 的 optional 值。

同樣的,你可以嘗試通過帶有 optional chaining 的下標設置新的值:

這個下標設置嘗試也會失敗,因為 residence 目前為 nil

如果你創建一個實際的 Residence 實例,並且將其分配給 john.residence,並且在 rooms 陣列中加入一個或多的 Room 實例,則可以使用 Residence 下標通過 optional chaining 訪問 rooms 陣列中的實際項目:

|訪問 Optional 類型的下標

如果下標返回的是 optional 類型的值(像是 Swift 的字典類型的 key 下標),則在下標的右括號後放置一個問號,以鏈接到其 optional 的返回值:

上面的範例定義了一個名為 testScores 的字典,該字典包含兩個 String key 映射到 [Int] value 的 key-value 組合。這個範例使用 optional chaining 將 “Dave” 陣列中的第一個項目設置為 91;將 “Bev” 陣列中的第一個項目增加 1;並且嘗試為 key 為 “Brain” 設置陣列中的第一個項目。前兩次調用成功,因為 testScores 字典包含 “Dave”“Bev” 的 key。第三次調用失敗,因為 testScores 字典不包含 "Brain” 的 key。

|連接多的層級的鏈

你可以將多個層級的 optional chaining 連接再一起,來深入到 Model 中更深的屬性、方法和下標中。但是,多個層級的 optional chaing 不會為返回的值添加更多層級的可選性。

也就是說:

  • 如果你嘗試檢索的類型不是 optional 的,則由於 optional chaining,它變為 optional 的。
  • 如果你嘗試檢索的類型已經是 optional 的,他不會因為 optional chaining 變得更 optional。

因此:

  • 如果你嘗試通過 optional chaining 檢索 Int 值,無論有多少層級的鏈接,總是返回 Int?
  • 同樣的,如果通過你嘗試 optional 檢索 Int? 值,無論有多少層級的鏈接,總是返回 Int?

下面的範例嘗試訪問 johnResidence 屬性中 address 屬性中的 street 屬性。這邊使用兩個層級的 optional chaining,來鏈接 residenceaddress 屬性,這兩者皆為 optional 類型:

john.residence 的值當前包含有效的 Residence 實例。但是,john.residence.address 的值為 nil。因此,對 john.residence?.address?.street 的調用失敗。

請注意,在上面的範例中,你嘗試檢索 street 屬性的值。這個屬性的類型為 String?。因此,即使除了屬性的基礎 optional 類型之外還應用了兩個層級的 optional chaining,john.residence?.address?.street 的返回值也是 String?

如果你將實際的 Address 實例設置為 john.residence.address 的值,並為該地址的 street 屬性設置了實際值,則可以通過多層級 optional chaining 訪問 street 屬性的值:

這個範例中,設置 john.residenceaddress 屬性的嘗試將會成功,因為 john.residence 的值當前包含有效的 Residence 實例。

|鏈結具有 Optional 返回值的方法

前面的範例演示了如何通過 optional chaining 檢索 optional 類型的屬性值。你還可以使用 optional chaining 來調用返回 optional 類型值的方法,並根據需要鏈接該方法的返回值。

下面的範例通過 optional chaining 調用 Address class 中的 buildingIdentifier 方法。這個方法返回 String? 類型的值。如上所述,在 optional chaining 後,此方法調用的最終返回類型也是 String?

如果你想要對該方法的返回值執行進一步的 optional chaining,請在該方法的括號後放置 optional chaining 的問號:

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]