2.16 可选链

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/optionalchaining/

2.16 可选链

无需解包即可访问可选值的成员。

可选链是在一个当前可能为 nil 的可选值上查询和调用属性、方法及下标的过程。如果可选值包含值,属性、方法或下标的调用就会成功;如果可选值是 nil,调用就返回 nil。多个查询可以串联起来,只要链条中有一环是 nil,整条链就会优雅地失败。

注意:Swift 中的可选链与在 Objective-C 中向 nil 发送消息类似, 但它的方式适用于任何类型,而且可以检查成功还是失败。

可选链作为强制解包的替代方案

当可选值非 nil 时,你想在它上面调用属性、方法或下标,就在这个可选值后面写一个问号(?)来指定可选链。这与在可选值后面写感叹号(!)强制解包它的值非常相似,主要区别是:可选值为 nil 时可选链会优雅地失败,而强制解包在可选值为 nil 时会触发运行时错误。

为了体现可选链可以在 nil 值上调用这一事实,可选链调用的结果始终是可选值,即使你查询的属性、方法或下标返回的是非可选值也是如此。你可以用这个可选返回值检查可选链调用是否成功(返回的可选值包含值),或者是否因为链条中出现 nil 而没有成功(返回的可选值是 nil)。

具体来说,可选链调用的结果在类型上与预期的返回值相同,但会被包在可选值中:一个通常返回 Int 的属性,通过可选链访问时会返回 Int?。

下面几段代码演示了可选链与强制解包的区别,以及如何检查调用是否成功。

首先定义两个类 Person 和 Residence:

1
2
3
4
5
6
7
class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence 实例有一个名为 numberOfRooms 的 Int 属性,默认值为 1。Person 实例有一个 Residence? 类型的可选属性 residence。

如果你创建一个新的 Person 实例,由于 residence 是可选类型,它默认被初始化为 nil。在下面的代码中,john 的 residence 属性值是 nil:

1
let john = Person()

如果你试图访问这个人住所的 numberOfRooms 属性——在 residence 后面写感叹号强制解包它的值——就会触发运行时错误,因为没有 residence 值可以解包:

1
2
let roomCount = john.residence!.numberOfRooms
// 这会触发运行时错误

当 john.residence 有非 nil 的值时,上面的代码会成功,并把 roomCount 设为包含相应房间数的 Int 值。但当 residence 为 nil 时,如上面所示,这段代码总是会触发运行时错误。

可选链提供了访问 numberOfRooms 值的另一种方式:用问号代替感叹号:

1
2
3
4
5
6
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// 输出 "Unable to retrieve the number of rooms."。

这告诉 Swift 要在可选的 residence 属性上"接链",并在 residence 存在时取出 numberOfRooms 的值。

由于这次访问 numberOfRooms 有可能失败,可选链尝试返回的是 Int? 类型的值,也就是"可选 Int"。当 residence 为 nil 时(如上面的例子),这个可选 Int 也是 nil,以体现无法访问 numberOfRooms 这一事实。随后通过可选绑定访问这个可选 Int,解包出整数并把非可选值赋给常量 roomCount。

注意,即使 numberOfRooms 是非可选的 Int,情况也是如此。通过可选链查询它,意味着对 numberOfRooms 的调用总是返回 Int? 而不是 Int。

你可以给 john.residence 赋一个 Residence 实例,让它不再是 nil:

1
john.residence = Residence()

现在 john.residence 包含一个真正的 Residence 实例,而不再是 nil。如果你用和之前相同的可选链访问 numberOfRooms,现在就会返回一个包含默认值 1 的 Int?:

1
2
3
4
5
6
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// 输出 "John's residence has 1 room(s)."。

为可选链定义模型类

可选链可以用于层级超过一层的属性、方法和下标调用。这让你能够深入访问相互关联的复杂类型模型中的子属性,并检查能否访问这些子属性上的属性、方法和下标。

下面这些代码片段定义了四个模型类,供后续几个例子使用,其中包括多级可选链的例子。这些类在上面的 Person 和 Residence 模型基础上增加了 Room 和 Address 类,以及相关的属性、方法和下标。

Person 类的定义与之前相同:

1
2
3
class Person {
    var residence: Residence?
}

Residence 类比之前更复杂了:这一次 Residence 类定义了一个名为 rooms 的变量属性,用一个 [Room] 类型的空数组初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 属性值。

作为访问 rooms 数组的快捷方式,这个版本的 Residence 提供了一个读写下标,用来访问 rooms 数组中指定下标的房间。

这个版本的 Residence 还提供了一个名为 printNumberOfRooms 的方法,它只是打印住所的房间数。

最后,Residence 定义了一个名为 address 的可选属性,类型为 Address?。这个属性所用的 Address 类类型定义如下。

rooms 数组所用的 Room 类是一个简单的类,只有一个名为 name 的属性,以及一个把该属性设为合适房间名的构造器:

1
2
3
4
class Room {
    let name: String
    init(name: String) { self.name = name }
}

这个模型中的最后一个类名为 Address。这个类有三个 String? 类型的可选属性:前两个属性 buildingName 和 buildingNumber 是标识地址中某一栋建筑的不同方式;第三个属性 street 用来表示该地址所在的街道名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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;如果 buildingNumber 和 street 都有值,就返回两者拼接的结果;否则返回 nil。

通过可选链访问属性

如可选链作为强制解包的替代方案所示,你可以用可选链访问可选值上的属性,并检查该属性访问是否成功。

用上面定义的类创建一个新的 Person 实例,并像之前那样尝试访问它的 numberOfRooms 属性:

1
2
3
4
5
6
7
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.")
}
// 输出 "Unable to retrieve the number of rooms."。

由于 john.residence 是 nil,这次可选链调用和之前一样失败了。

你也可以尝试通过可选链设置属性的值:

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

在这个例子里,设置 john.residence 的 address 属性的尝试会失败,因为 john.residence 目前是 nil。

赋值是可选链的一部分,这意味着 = 运算符右侧的代码不会被求值。在上一个例子里,不太容易看出 someAddress 从未被求值,因为访问一个常量没有任何副作用。下面的代码做同样的赋值,但用函数来创建地址:这个函数在返回值之前会打印 “Function was called”,从而让你看到 = 右侧是否被求值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func createAddress() -> Address {
    print("Function was called.")

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

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

你可以看出 createAddress() 函数没有被调用,因为什么都没有打印。

通过可选链调用方法

你可以用可选链在可选值上调用方法,并检查方法调用是否成功,即使该方法没有定义返回值也可以这样做。

Residence 类上的 printNumberOfRooms() 方法打印 numberOfRooms 的当前值,该方法如下:

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

这个方法没有指定返回类型。不过,没有返回类型的函数和方法有一个隐式返回类型 Void,详见无返回值的函数。这意味着它们返回的值是 (),也就是空元组。

如果你用可选链在可选值上调用这个方法,方法的返回类型会是 Void? 而不是 Void,因为通过可选链调用时返回值总是可选类型。这让你能够用 if 语句检查是否可以调用 printNumberOfRooms() 方法,即使这个方法本身没有定义返回值。把 printNumberOfRooms 调用的返回值与 nil 比较,即可判断方法调用是否成功:

1
2
3
4
5
6
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.")
}
// 输出 "It was not possible to print the number of rooms."。

通过可选链设置属性时也是如此。上面通过可选链访问属性中的例子试图给 john.residence 设置 address 值,尽管 residence 属性是 nil。任何通过可选链设置属性的尝试都返回 Void? 类型的值,这让你可以与 nil 比较,判断属性是否设置成功:

1
2
3
4
5
6
if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
// 输出 "It was not possible to set the address."。

通过可选链访问下标

你可以用可选链尝试从可选值上的下标读取和设置值,并检查下标调用是否成功。

注意:通过可选链访问可选值上的下标时, 问号要放在下标方括号的前面,而不是后面。 可选链的问号总是紧跟在表达式中可选部分之后。

下面的例子用 Residence 类定义的下标,尝试取出 john.residence 属性中 rooms 数组里第一个房间的名字。由于 john.residence 目前是 nil,这次下标调用失败:

1
2
3
4
5
6
if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// 输出 "Unable to retrieve the first room name."。

这次下标调用中的可选链问号紧跟在 john.residence 之后、下标方括号之前,因为 john.residence 才是要施加可选链的那个可选值。

类似地,你也可以尝试通过带可选链的下标设置新值:

1
john.residence?[0] = Room(name: "Bathroom")

这次下标设置同样失败,因为 residence 目前是 nil。

如果你给 john.residence 创建并赋一个真正的 Residence 实例,并在它的 rooms 数组中放一个或多个 Room 实例,就可以通过可选链用 Residence 下标访问 rooms 数组中真正的元素:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// 输出 "The first room name is Living Room."。

访问可选类型的下标

如果下标返回的值是可选类型——例如 Swift Dictionary 类型的键下标——就把问号放在下标右方括号之后,对它的可选返回值接链:

1
2
3
4
5
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// "Dave" 数组现在是 [91, 82, 84],"Bev" 数组现在是 [80, 94, 81]

上面的例子定义了一个名为 testScores 的字典,其中包含两个键值对,把 String 键映射到 Int 值数组。这个例子用可选链把 "Dave" 数组的第一项设为 91,把 "Bev" 数组的第一项加 1,并尝试设置 "Brian" 键对应数组的第一项。前两次调用成功,因为 testScores 字典包含 "Dave" 和 "Bev" 两个键;第三次调用失败,因为 testScores 字典中没有 "Brian" 这个键。

串联多层链

你可以把多层可选链串联起来,深入访问模型中更深处的属性、方法和下标。不过,多层可选链并不会给返回值增加更多层的可选性。

换句话说:

  • 如果你要取的类型不是可选类型,它会因为可选链而变成可选类型。
  • 如果你要取的类型本来就是可选类型,它不会因为接链而变得更加可选。

因此:

  • 如果你通过可选链取一个 Int 值,无论接了多少层链,返回的总是 Int?。
  • 类似地,如果你通过可选链取一个 Int? 值,无论接了多少层链,返回的总是 Int?。

下面的例子尝试访问 john 的 residence 属性中 address 属性的 street 属性。这里用了两层可选链,分别穿过 residence 和 address 这两个可选类型的属性:

1
2
3
4
5
6
if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}
// 输出 "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 属性的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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.")
}
// 输出 "John's street name is Laurel Street."。

在这个例子里,设置 john.residence 的 address 属性的尝试会成功,因为 john.residence 的值目前包含一个有效的 Residence 实例。

在返回可选值的方法上接链

前面的例子展示了如何通过可选链取出可选类型属性的值。你也可以用可选链调用返回可选类型值的方法,并在需要时对该方法的返回值接链。

下面的例子通过可选链调用 Address 类的 buildingIdentifier() 方法。该方法返回 String? 类型的值。如上所述,经过可选链之后,这个方法调用的最终返回类型同样是 String?:

1
2
3
4
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// 输出 "John's building identifier is The Larches."。

如果你想在这个方法的返回值上继续使用可选链,就把可选链的问号放在方法圆括号之后:

1
2
3
4
5
6
7
8
9
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 doesn't begin with \"The\".")
    }
}
// 输出 "John's building identifier begins with "The"."。

注意:在上面的例子里, 可选链问号放在圆括号之后, 因为你要接链的可选值是 buildingIdentifier() 方法的返回值, 而不是 buildingIdentifier() 方法本身。