Swift 可選鏈

可選鏈(Optional Chaining)是一種可以請求和呼叫屬性、方法和子指令碼的過程,用於請求或呼叫的目標可能為nil。

可選鏈返回兩個值:

  • 如果目標有值,呼叫就會成功,返回該值

  • 如果目標為nil,呼叫將返回nil

多次請求或呼叫可以被連結成一個鏈,如果任意一個節點為nil將導致整條鏈失效。


可選鏈可替代強制解析

通過在屬性、方法、或下標指令碼的可選值後面放一個問號(?),即可定義一個可選鏈。

可選鏈 '?' 感嘆號(!)強制展開方法,屬性,下標指令碼可選鏈
? 放置於可選值後來呼叫方法,屬性,下標指令碼 ! 放置於可選值後來呼叫方法,屬性,下標指令碼來強制展開值
當可選為 nil 輸出比較友好的錯誤資訊 當可選為 nil 時強制展開執行錯誤

使用感嘆號(!)可選鏈例項

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

//將導致執行時錯誤
let roomCount = john.residence!.numberOfRooms

以上程式執行輸出結果為:

fatal error: unexpectedly found nil while unwrapping an Optional value

想使用感嘆號(!)強制解析獲得這個人residence屬性numberOfRooms屬性值,將會引發執行時錯誤,因為這時沒有可以供解析的residence值。

使用問號(?)可選鏈例項

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

// 連結可選residence?屬性,如果residence存在則取回numberOfRooms的值
if let roomCount = john.residence?.numberOfRooms {
    print("John 的房間號為 \(roomCount)。")
} else {
    print("不能檢視房間號")
}

以上程式執行輸出結果為:

不能檢視房間號

因為這種嘗試獲得numberOfRooms的操作有可能失敗,可選鏈會返回Int?型別值,或者稱作"可選Int"。當residence是空的時候(上例),選擇Int將會為空,因此會出現無法訪問numberOfRooms的情況。

要注意的是,即使numberOfRooms是非可選Int(Int?)時這一點也成立。只要是通過可選鏈的請求就意味著最後numberOfRooms總是返回一個Int?而不是Int。


為可選鏈定義模型類

你可以使用可選鏈來多層呼叫屬性,方法,和下標指令碼。這讓你可以利用它們之間的複雜模型來獲取更底層的屬性,並檢查是否可以成功獲取此類底層屬性。

例項

定義了四個模型類,其中包括多層可選鏈:

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

通過可選鏈呼叫方法

你可以使用可選鏈的來呼叫可選值的方法並檢查方法呼叫是否成功。即使這個方法沒有返回值,你依然可以使用可選鏈來達成這一目的。

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()


if ((john.residence?.printNumberOfRooms()) != nil) {
    print("輸出房間號")
} else {
    print("無法輸出房間號")
}

以上程式執行輸出結果為:

無法輸出房間號

使用if語句來檢查是否能成功呼叫printNumberOfRooms方法:如果方法通過可選鏈呼叫成功,printNumberOfRooms的隱式返回值將會是Void,如果沒有成功,將返回nil。


使用可選鏈呼叫下標指令碼

你可以使用可選鏈來嘗試從下標指令碼獲取值並檢查下標指令碼的呼叫是否成功,然而,你不能通過可選鏈來設定下標指令碼。

例項1

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()
if let firstRoomName = john.residence?[0].name {
    print("第一個房間名 \(firstRoomName).")
} else {
    print("無法檢索到房間")
}

以上程式執行輸出結果為:

無法檢索到房間

在下標指令碼呼叫中可選鏈的問號直接跟在 john.residence 的後面,在下標指令碼括號的前面,因為 john.residence 是可選鏈試圖獲得的可選值。

例項2

例項中建立一個 Residence 例項給 john.residence,且在他的 rooms 陣列中有一個或多個 Room 例項,那麼你可以使用可選鏈通過 Residence 下標指令碼來獲取在 rooms 陣列中的例項了:

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse

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

if let johnsStreet = john.residence?.address?.street {
    print("John 所在的街道是 \(johnsStreet)。")
} else {
    print("無法檢索到地址。 ")
}

以上程式執行輸出結果為:

John 所在的街道是 Laurel Street。

通過可選連結呼叫來訪問下標

通過可選連結呼叫,我們可以用下標來對可選值進行讀取或寫入,並且判斷下標呼叫是否成功。

例項

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("第一個房間名為\(firstRoomName)")
} else {
    print("無法檢索到房間")
}

以上程式執行輸出結果為:

第一個房間名為客廳

訪問可選型別的下標

如果下標返回可空型別值,比如Swift中Dictionary的key下標。可以在下標的閉合括號後面放一個問號來連結下標的可空返回值:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0]++
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]

上面的例子中定義了一個testScores陣列,包含了兩個鍵值對, 把String型別的key對映到一個整形陣列。

這個例子用可選連結呼叫把"Dave"陣列中第一個元素設為91,把"Bev"陣列的第一個元素+1,然後嘗試把"Brian"陣列中的第一個元素設為72。

前兩個呼叫是成功的,因為這兩個key存在。但是key"Brian"在字典中不存在,所以第三個呼叫失敗。


連線多層連結

你可以將多層可選鏈連線在一起,可以掘取模型內更下層的屬性方法和下標指令碼。然而多層可選鏈不能再新增比已經返回的可選值更多的層。

如果你試圖通過可選鏈獲得Int值,不論使用了多少層連結返回的總是Int?。 相似的,如果你試圖通過可選鏈獲得Int?值,不論使用了多少層連結返回的總是Int?。

例項1

下面的例子試圖獲取john的residence屬性裡的address的street屬性。這裡使用了兩層可選鏈來聯絡residence和address屬性,它們兩者都是可選型別:

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()

if let johnsStreet = john.residence?.address?.street {
    print("John 的地址為 \(johnsStreet).")
} else {
    print("不能檢索地址")
}

以上程式執行輸出結果為:

不能檢索地址

例項2

如果你為Address設定一個例項來作為john.residence.address的值,併為address的street屬性設定一個實際值,你可以通過多層可選鏈來得到這個屬性值。

class Person {
   var residence: Residence?
}

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("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

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

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}
let john = Person()
john.residence?[0] = Room(name: "浴室")

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "客廳"))
johnsHouse.rooms.append(Room(name: "廚房"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("第一個房間是\(firstRoomName)")
} else {
    print("無法檢索房間")
}

以上例項輸出結果為:

第一個房間是客廳

對返回可選值的函式進行連結

我們還可以通過可選連結來呼叫返回可空值的方法,並且可以繼續對可選值進行連結。

例項

class Person {
    var residence: Residence?
}

// 定義了一個變數 rooms,它被初始化為一個Room[]型別的空陣列
class Residence {
    var rooms = [Room]()
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        return rooms[i]
    }
    func printNumberOfRooms() {
        print("房間號為 \(numberOfRooms)")
    }
    var address: Address?
}

// Room 定義一個name屬性和一個設定room名的初始化器
class Room {
    let name: String
    init(name: String) { self.name = name }
}

// 模型中的最終類叫做Address
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if (buildingName != nil) {
            return buildingName
        } else if (buildingNumber != nil) {
            return buildingNumber
        } else {
            return nil
        }
    }
}

let john = Person()

if john.residence?.printNumberOfRooms() != nil {
    print("指定了房間號)")
}  else {
    print("未指定房間號")
}

以上程式執行輸出結果為:

未指定房間號