Swift 5.x - 可选链接(中文文档)

905 阅读13分钟

引言

继续学习Swift文档,从上一章节:析构函数,我们学习了Swift析构函数相关的内容。现在,我们学习Swift可选链接的相关的内容。由于篇幅较长,这里分篇来记录,接下来,Fighting!

如果你已经熟悉这部分内容,移步下一章节:错误处理

可选链接

可选链接是一个查询和调用当前可能为零的可选属性,方法和下标的过程。 如果可选包含值,则属性,方法或下标调用成功。 如果可选值为nil,则属性,方法或下标调用将返回nil。 可以将多个查询链接在一起,如果链中的任何链接为nil,则整个链都会失败。

注意
Swift中的可选链接类似于Objective-C中的无消息传递,但是它适用于任何类型,并且可以检查成功或失败。

1 可选链接作为强制展开的替代方法

如果可选值不为nil,可以通过在要调用属性,方法或下标的可选值之后放置问号(?)来指定可选链。这非常类似于将感叹号(!)放在可选值之后以强制展开其值。主要区别在于,当可选值为nil时,强制展开会触发运行时错误。

为了反映可以在nil值上调用可选链接的事实,即使要查询的属性,方法或下标返回的是非可选值,可选链接调用的结果也始终是可选值。您可以使用此可选返回值来检查可选链接调用是否成功(返回的可选包含一个值),或者由于链中的值为nil而失败(返回的可选值为nil)。

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

接下来的几个代码片段演示了可选链接与强制展开如何不同,并使您能够检查是否成功。

首先,定义了两个类,分别是Person和Residence:

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

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

如果创建新的Person实例,则其residence属性由于是可选属性而默认初始化为nil。 在下面的代码中,john的住宅属性值为nil:

let john = Person()

如果您尝试访问此人的residence的numberOfRooms属性,方法是在residence后放置一个感叹号以使其值解开,从而触发运行时错误,因为没有要解开的residence值:

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

上面的代码在john.residence具有非nil值时成功,并将roomCount设置为包含适当房间数的Int值。 但是,如上所示,此代码总是在驻留时间为nil时触发运行时错误。

可选链接提供了一种访问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”。 如上例所示,当Residence为nil时,此可选的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)."

2 定义可选链接的模型类

您可以将可选链接与深度超过一级的属性,方法和下标一起使用。 这使您可以深入研究相互关联类型的复杂模型中的子属性,并检查是否可以访问这些子属性上的属性,方法和下标。

下面的代码段定义了四个模型类,可用于后续的几个示例中,包括多级可选链接的示例。 这些类通过添加带有相关属性,方法和下标的Room和Address类,从上面扩展了Person and Residence模型。

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属性的值。

作为访问其房间数组的捷径,此版本的Residence提供了一个可读写的下标,该下标提供了对房间数组中请求的索引的访问。

此版本的Residence还提供了一种名为printNumberOfRooms的方法,该方法仅打印该房间的房间数量。

最后,Residence定义了一个名为address的可选属性,其类型为Address?。 此属性的Address类类型在下面定义。

用于rooms数组的Room类是一个简单的类,具有一个名为name的属性,以及一个用于将该属性设置为合适的房间名称的初始化方法:

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

此模型中的最后一类称为地址。 此类具有String?类型的三个可选属性。 前两个属性buildingName和buildingNumber是将特定建筑物标识为地址一部分的替代方法。 第三个属性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。

3 通过可选链接访问属性

如“可选链接”作为强制展开的替代中所演示的,您可以使用可选链接来访问可选值上的属性,并检查该属性访问是否成功。

使用上面定义的类创建一个新的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.residence为nil,因此此可选链接与前面相同的方式,调用失败。

您还可以尝试通过可选的链接设置属性的值:

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

在此示例中,尝试设置john.residence的地址属性将失败,因为john.residence当前为nil。

赋值是可选链接的一部分,这意味着=运算符右侧的任何代码都不会被调用。 在上一个示例中,不容易发现someAddress从未被调用,因为访问常量没有任何反应。 下面的清单执行相同的分配,但是它使用一个函数来创建地址。 该函数在返回值之前打印“调用了函数”,这使您可以查看=运算符的右侧是否已被求值。

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

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

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

您可以说未调用createAddress()函数,因为未打印任何内容。

4 通过可选链接调用方法

您可以使用可选链接来对可选值调用方法,并检查该方法调用是否成功。 即使该方法未定义返回值,也可以执行此操作。

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

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

此方法未指定返回类型。 但是,没有返回类型的函数和方法具有隐式的Void返回类型,如 Functions Without Return Values中所述。 这意味着它们返回值()或一个空的元组。

如果您通过可选链接对可选值调用此方法,则该方法的返回类型将为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设置地址值,即使Residence属性为nil。 任何通过可选链接设置属性的尝试都会返回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."

5 通过可选链接访问下标

您可以使用可选链接尝试从下标中检索和设置可选值的值,并检查该下标调用是否成功。

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

下面的示例尝试使用Residence类定义的下标检索john.residence属性的rooms数组中第一个房间的名称。 因为john.residence当前为nil,所以下标调用失败:

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

此下标调用中的可选链接问号放在john.residence之后,在下标括号之前,因为john.residence是尝试进行可选链接的可选值。

同样,您可以尝试通过带有可选链接的下标设置新值:

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

该下标设置尝试也将失败,因为当前居住地为nil。

如果您创建一个实际的Residence实例并将其分配给john.residence,并且在rooms数组中包含一个或多个Room实例,则可以使用Residence下标通过可选的链接访问Rooms数组中的实际项目:

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.")
}
// Prints "The first room name is Living Room."

5.1 访问可选类型的下标

如果下标返回的是可选类型的值(例如Swift的Dictionary类型的关键下标),则在下标的右括号后面放置一个问号,以链接到其可选的返回值上:

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

上面的示例定义了一个名为testScores的字典,该字典包含两个将字符串键映射到Int值数组的键值对。 该示例使用可选的链接将“ Dave”数组中的第一项设置为91。 将“ Bev”数组中的第一项增加1; 并尝试为“ Brian”键设置数组中的第一项。 前两个调用成功,因为testScores词典包含“ Dave”和“ Bev”的键。 第三次调用失败,因为testScores词典不包含“ Brian”的键。

6 链接多个级别的链接

您可以将多个级别的可选链接链接在一起,以深入挖掘模型中更深的属性,方法和下标。 但是,多个级别的可选链接不会为返回的值添加更多级别的可选性。

换一种方式:

  • 如果您尝试检索的类型不是可选的,则由于可选的链接,它将变为可选的。
  • 如果您尝试检索的类型已经是可选的,则由于链接的原因,它将不再是可选的。

因此:

  • 如果您尝试通过可选链接检索Int值,则为Int? 无论使用多少级链接,总是返回。
  • 同样,如果您尝试检索一个Int? 通过可选的链接(Int)获得Int? 无论使用多少级链接,总是返回。

下面的示例尝试访问john的Residence属性的address属性的street属性。 此处使用两个级别的可选链接,以链接驻留和地址属性,两者均为可选类型:

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实例。

7 链接具有可选返回值的方法

前面的示例显示了如何通过可选链接检索可选类型的属性的值。 您还可以使用可选链接来调用返回可选类型值的方法,并根据需要链接该方法的返回值。

下面的示例通过可选的链接调用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()方法本身。

总结

这章内容主要讲了可选链接的作用和用法,涉及到“?”和“!”这两个关键符号,通过使用?,来判断是否有值或者为nil,使用可选绑定if-let配合来判断调用哪个逻辑。使用!强制解包一定有值的属性,若为nil,会报错;这部分内容理解就好。

喜欢的朋友麻烦动动你的🤚,点个👍哦,谢谢~

上一章节:析构函数

下一章节:错误处理

参考文档:Swift - Optional Chaining