iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 7
0
自我挑戰組

iOS 新手開發的大小事系列 第 7

Day 7: [Swift] 可選鏈 (Optional Chaining)

  • 分享至 

  • xImage
  •  

前言

昨天介紹了可選型別 (Optional),今天就針對可選鏈 (Optional Chaining),來加以介紹,目前對於可選鏈還沒有深刻的體會,就目前的理解,大概知道是用此方法可以代替昨天所述的強制解包,因強制解包,在開發者不確定是否肯定有值得情況下,容易造成運行時的錯誤,則可以用可選鏈作為替代方案,還有其他情況下的介紹,先在此紀錄,以便之後運用時可方便查詢。

可選鏈是一個查詢和調用當前可能為 nil 的可選型別之屬性、方法和下標的過程。如果可選型別包含值,則屬性、方法或下標調用成功;如果可選值為 nil,則屬性、方法或下標調用將返回 nil。可以將多個查詢鏈接在一起,如果鏈中的任何鏈為 nil,則整個鏈會優雅的失敗。

Swift 中的可選鏈類似於 Objective-C 中的 nil 消息傳遞,但是它適用於任何類型,並且可以檢查成功或失敗。


可選鏈代替強制解包 (Optional Chaining as an Alternative to Forced Unwrapping)

可以通過在要調用其屬性、方法或下標的可選值(如果可選值不為 nil)之後放置問號 (?) 來指定可選鏈。這與在可選值之後放置驚嘆號 (!) 以強制展開其值非常相似。主要差別在於,當可選值為 nil 時,可選鏈接會優雅的失敗,而當可選值為 nil 時,強制解包會觸發運行時錯誤。

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

具體來說,可選鏈調用的結果與預期返回值的型別相同,但包裝在可選型別中。通過可選鏈訪問時,返回 Int 屬性將返回 Int?。

接下來的幾段代碼展示了可選鏈與強制解包如何不同,並能夠檢查是否成功。

首先,定義了兩個類,分別是 PersonResidence

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence 實例具有一個名為 numberOfRooms 的 Int 屬性,預設值為 1。Person 實例具有型別為 Residence? 的屬性可選型別 Residence

如果創建新的 Person 實例,則其屬性 residence 因為是可選型別而預設初始化為 nil。在下面的代碼中,john 的屬性 residence 值為 nil

let john = Person()

如果嘗試訪問此人的住所的屬性 numberOfRooms,方法是在住所後放置一個驚嘆號以強制其值解開,從而觸發運行時錯誤,因為沒有 residence 值可以解開:

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

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

可選鏈提供了一種訪問 numberOfRooms 值的替代方法。要使用可選鏈,請使用問號代替驚嘆號:

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

這告訴 Swift 可以在屬性可選型別 Residence 上「鏈接」,並且如果 residence 存在則取回 numberOfRooms 的值。

由於嘗試訪問 numberOfRooms 可能會失敗,因此可選鏈嘗試返回 Int? 型別的值,或 “optional Int” 型別。如上例所示,當 Residencenil 時,此可選的 Int 也將為 nil,以反映無法訪問 numberOfRooms 的事實。通過可選綁定以訪問可選 Int 可以解開整數並將非可選的值分配給 roomCount 變數。

請注意,即使 numberOfRooms 是非可選 Int,也是如此。通過可選鏈查詢它的事實意味著對 numberOfRooms 的調用將始終返回 Int? 而不是 Int。

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

john.residence = Residence()

john.residence 現在包含一個實際的 Residence 實例,而不是 nil。如果嘗試使用與以前相同的可選鏈來訪問 numberOfRooms,現在它將返回一個 Int? 包含預設的 numberOfRooms 值 1:

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."

定義可選鏈的模型類 (Defining Model Classes for Optional Chaining)

可將可選鏈與深度超過一層的屬性、方法和下標一起使用。這使我們可以深入相互關聯型別的複雜模型中的子屬性,並檢查是否可以訪問這些子屬性上的屬性、方法和下標。

下面的代碼定義了四個模型類,可用於後續的幾個範例中,包括多級可選鏈的範例。這些類透過添加帶有相關屬性、方法和下標的類 RoomAddress ,從上面擴展了 PersonResidence 模型。

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

class Person {
    var residence: Residence?
}

Residence 比以前複雜得多。這次,類 Residence 定義了一個名為 Rooms 的變數屬性,該屬性使用 [Room] 型別的空數組初始化:

class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    func printNumberOfRooms() {
        print("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?
}

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

作為訪問其數組 room 的捷徑,此版本的 Residence 提供了一個可讀寫的下標,該下標提供了對數組 room 中請求索引的訪問。

此版本的 Residence 還提供了一個名為 printNumberOfRooms 的方法,該方法僅印出該房間的房間數量。

最後,Residence 定義了一個名為 address 的可選屬性,其型別為 Address?。此屬性的類 Address 型別在下面定義。

用於數組 rooms 的類 Room 是一個簡單的類,具有一個名為 name 的屬性,以及一個用於將該屬性設置合適的房間名稱的初始化程序:

class Room {
    let name: String
    init(name: String) { self.name = name }
}

此模型中的最後一類稱為 Address。此類具有三個 String? 型別的可選屬性。前兩個屬性 buildingNamebuildingNumber 是將特定建築物標識為地址一部分的替代方法。第三個屬性 street 用來為該地址命名街道:

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        } else if buildingName != nil {
            return buildingName
        } else {
            return nil
        }
    }
}

Address 還提供了一個名為 buildingIdentifier() 的方法,該方法的返回類型為 String?。此方法檢查地址的屬性,如果有值則返回 buildingName,如果有值則返回與 street 串聯的 buildingNumber,否則返回 nil


通過可選鏈訪問屬性 (Accessing Properties Through Optional Chaining)

如可選鏈代替強制展開章節中所示,可以使用可選鏈訪問可選值的屬性,並檢查該屬性訪問是否成功。

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

let john = Person()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

由於 john.residencenil,因此此可選的鏈調用以與以前相同的方式失敗。

還可以嘗試通過可選的鏈設置屬性的值:

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

在此範例中,嘗試設置 john.residence 的地址屬性將失敗,因為 john.residence 當前為 nil

賦值是可選鏈的一部分,這意味著 "=" 運算符右側的任何代碼都不會被評估。在上一個範例中,不容易看到 someAddress 從未被評估,因為訪問常數沒有任何副作用。下面的清單執行相同的指定,但是它使用一個函數來創建地址。該函數在返回值之前印出 “Function was called”,這使我們可以查看 "=" 運算符的右側是否已被求值。

func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()

可以說未調用 createAddress() 函數,因為未印出任何內容。


通過可選鏈調用方法 (Calling Methods Through Optional Chaining)

可以使用可選鏈來對在可選值上的方法進行調用,並檢查該方法調用是否成功。即使該方法未定義返回值,也可以執行此操作。

Residence 中的 printNumberOfRooms() 方法將印出 numberOfRooms 的當前值。該方法如下:

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

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

透過過可選鏈對可選值調用此方法,則該方法的返回類型將為 Void?,而不是 Void,因為透過可選鏈接調用時,返回值始終為可選型別。可以使用 if 語句檢查是否可以調用printNumberOfRooms() 方法,即使該方法本身並未定義返回值。將 printNumberOfRooms 調用的返回值與 nil 進行比較,以查看方法調用是否成功:

if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")
}
// Prints "It was not possible to print the number of rooms."

如果嘗試通過可選鏈設置屬性,也是如此。上面的「通過可選鏈訪問屬性」中的範例嘗試為 john.residence 設置地址值,即使屬性 Residencenil。任何通過可選鏈設置屬性的嘗試都會返回 Void 型別的值,則可以與 nil 進行比較以查看是否成功設置了該屬性:

if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
// Prints "It was not possible to set the address."

鏈的多層的連接 (Linking Multiple Levels of Chaining)

可以將多個層級的可選鏈連接在一起,以深入到模型中更深的屬性、方法和下標。但是,多個層級的可選鏈不會為返回的值添加更多層級的可選性。

換一種方式描述:

  • 如果您嘗試取回的型別不是可選的,則由於可選鏈的關係,它將變為可選的。
  • 如果您嘗試取回的型別已經是可選的,那麼由於鏈的原因,它不會變得更加可選。

因此:

  • 如果嘗試通過可選鏈返回 Int 值,則總是返回 Int? 無論使用多少層級鏈。
  • 同樣,如果嘗試通過可選鏈返回一個 Int?,則總是返回 Int? 無論使用多少層級鏈。

下面的範例嘗試訪問 john 的屬性 Residence 的屬性 address 的屬性 street。此處使用兩層級的可選鏈,以鏈接屬性 residenceaddress,這兩種均為可選類型:

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}
// Prints "Unable to retrieve the address."

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

請注意,在上面的範例中,嘗試返回屬性 street 的值。此屬性的型別為 String?。因此,即使除了屬性的基礎可選型別之外還應用了兩層級的可選鏈,john.residence?.address?.street 的返回值也是 String?

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

let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}
// Prints "John's street name is Laurel Street."

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


鏈具有可選返回值的方法 (Chaining on Methods with Optional Return Values)

前面的範例顯示如何通過可選鏈返回可選型別屬性的值。還可以使用可選鏈來調用方法返回可選型別值,並根據需要在該方法上的鏈來返回值。

下面的範例通過可選鏈調用類 Address 的方法buildingIdentifier()。此方法返回 String? 型別的值。如上所述,在可選鏈之後,此方法調用的最終返回型別也是 String?:

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."

如果要對該方法的返回值執行進一步的可選鏈,請在該方法的括號後放置可選鏈問號:

if let beginsWithThe =
    john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
    if beginsWithThe {
        print("John's building identifier begins with \"The\".")
    } else {
        print("John's building identifier does not begin with \"The\".")
    }
}
// Prints "John's building identifier begins with "The"."

在上面的範例中,將可選鏈問號放在括號後,因為要鏈接的可選值是方法 buildingIdentifier() 的返回值,而不是方法 buildingIdentifier() 本身。


上一篇
Day 6: [Swift] 可選型別 (Optional)
下一篇
Day 8: [Swift] 型別轉換 (Type Casting)
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言