可选的链接
可选链接是查询和调用属性、方法和下标的过程,这些可选对象目前可能为nil。如果可选项包含一个值,则该属性、方法或下标调用成功;如果可选为nil,则属性、方法或下标调用返回nil。多个查询可以链接在一起,如果链中的任何链接是nil,那么整个链会优雅地失败。
Swift中的可选链接类似于Objective-C中的消息传递nil,但在某种程度上适用于任何类型,并且可以检查是否成功或失败。
可选链接作为强制展开的替代选择
如果可选值为非nil,则在您希望在其上调用属性、方法或下标的可选值后面放置问号(?),从而指定可选链接。这与在可选值后放置感叹号(!)以强制展开其值非常相似。主要的区别是,可选链接在可选为nil时优雅地失败,而强制展开则在可选为nil时触发运行时错误。
为了反映可选链接可以在nil值上调用的事实,可选链接调用的结果总是可选值,即使您查询的属性、方法或下标返回非可选值。您可以使用这个可选的返回值来检查可选链接调用是否成功(返回的可选包含一个值),或者由于链中的nil值而没有成功(返回的可选值是nil)。
为了反映可选链接可以在nil值上调用的事实,可选链接调用的结果总是可选值,即使您查询的属性、方法或下标返回非可选值。您可以使用这个可选的返回值来检查可选链接调用是否成功(返回的可选包含一个值),或者由于链中的nil值而没有成功(返回的可选值是nil)。
具体来说,可选链接调用的结果与预期的返回值类型相同,但封装在可选调用中。通常返回Int的属性将返回Int?通过可选链接访问时。
接下来的几个代码片段演示了可选链接与强制展开的区别,并使您能够检查是否成功。
首先,定义了两个类Person和Residence:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
住宅实例有一个名为numberOfRooms的Int属性,默认值为1。Person实例具有类型为residence ?的可选住宅属性。
如果您创建一个新的Person实例,它的驻留属性默认初始化为nil,因为它是可选的。在下面的代码中,john的住宅物业价值为nil:
let john = Person()
如果您试图访问这个人的住宅的numberOfRooms属性,在住宅之后放置一个惊叹号来强制展开它的值,您会触发一个运行时错误,因为没有要展开的住宅值:
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error
上面的代码在john的时候成功了。住宅具有非nil值,并将roomCount设置为一个Int值,其中包含适当的房间数量。然而,当驻留为nil时,该代码总是会触发运行时错误,如上所示。
可选链接提供了访问多个房间值的另一种方法。若要使用可选链接,请使用问号代替感叹号:
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在可选住宅属性上进行“链”,并在存在住宅的情况下检索多个房间的值。
因为访问numberOfRooms的尝试有可能失败,所以可选的链接尝试返回Int类型的值?,或“可选Int”。当residence是nil时,如上面的示例所示,这个可选的Int也将是nil,以反映无法访问numberOfRooms的事实。通过可选绑定访问可选Int,以解包整型并将非可选值分配给roomCount变量。
注意,尽管numberOfRooms是一个非可选的Int型函数,但它是通过可选链查询的,这意味着对numberOfRooms的调用总是会返回Int型函数?而不是Int。
您可以为john分配一个驻留实例。住宅,使其不再具有零值:
john.residence = Residence()
john.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)."
为可选链接定义模型类
可以使用可选的链接,调用深度超过一层的属性、方法和下标。这使您能够深入研究相互关联类型的复杂模型中的子属性,并检查是否可以访问这些子属性上的属性、方法和下标。
下面的代码片段定义了四个模型类,供后面的几个示例使用,包括多级可选链接的示例。这些类通过添加一个房间和地址类,以及相关的属性、方法和下标,从上面扩展了Person和Residence模型。
Person类的定义与前面相同:
class Person {
var 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?
}
因为这个版本的驻留存储了一个房间实例数组,所以它的numberOfRooms属性实现为计算属性,而不是存储属性。computed numberOfRooms属性只是从rooms数组返回count属性的值。
作为访问其rooms数组的快捷方式,这个版本的Residence提供了一个读写下标,它提供了访问rooms数组中请求索引的房间的权限。
这个版本的住宅还提供了一个名为printNumberOfRooms的方法,它只是简单地打印了住宅中的房间数量。
最后,Residence定义了一个名为address的可选属性,其类型为address ?。此属性的地址类类型定义如下。
用于rooms数组的Room类是一个简单的类,具有一个名为name的属性,以及一个初始化器,用于将该属性设置为合适的Room名称:
class Room {
let name: String
init(name: String) { self.name = name }
}
这个模型中的最后一个类称为Address。这个类有三个字符串类型的可选属性。前两个属性,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;如果两者都有值,就返回buildingNumber,或者返回nil。
通过可选链接访问属性
如可选链接作为强制展开的替代方法所示,您可以使用可选链接访问可选值上的属性,并检查该属性访问是否成功。
使用上面定义的类来创建一个新的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的address属性。定居将会失败,因为john.residence目前为nil。
赋值是可选链接的一部分,这意味着=操作符右侧的代码都没有求值。在前面的示例中,很难看到someAddress从未求值,因为访问常量没有任何副作用。下面的清单执行相同的赋值,但是它使用一个函数来创建地址。函数在返回值之前打印“函数被调用”,这可以让您看到=运算符的右侧是否被求值。
赋值是可选链接的一部分,这意味着=操作符右侧的代码都没有求值。在前面的示例中,很难看到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()函数没有被调用,因为没有打印任何内容。
通过可选链接调用方法
可以使用可选链接对可选值调用方法,并检查该方法调用是否成功。即使该方法没有定义返回值,也可以这样做。
驻留类上的printNumberOfRooms()方法打印当前的numberOfRooms值。下面是这个方法的样子:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
此方法没有指定返回类型。但是,没有返回类型的函数和方法有一个隐式的返回类型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设置地址值来访问属性。住宅,即使住宅财产为零。任何通过可选链接设置属性的尝试返回类型为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."
通过可选链接访问下标
您可以使用可选链接尝试从可选值的下标中检索和设置值,并检查该下标调用是否成功。
当您通过可选链接访问可选值上的下标时,您将问号放在下标的括号前面,而不是后面。可选的链接问号总是紧跟在可选表达式的后面。
下面的示例尝试检索john的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。
如果您为john创建并分配一个实际的驻留实例。驻留,在其rooms数组中有一个或多个Room实例,您可以使用驻留下标通过可选链接访问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."
访问可选类型的下标
如果下标返回可选类型的值——比如Swift字典类型的关键下标——在下标的右括号后面加一个问号,链接到可选返回值上:
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”的键。
链接多层次的链接
您可以将多个级别的可选链接链接在一起,以深入到模型中的属性、方法和下标。但是,多个可选链接级别不会为返回值添加更多的可选级别。
换句话说:
如果您尝试检索的类型不是可选的,那么由于可选链接,它将成为可选的。
如果您尝试检索的类型已经是可选的,那么它将不会因为链接而变得更可选。
因此:
如果您试图通过可选的链接检索Int值,Int?总是返回,不管使用了多少级别的链接。
类似地,如果尝试检索Int?值通过可选的链接,一个Int?总是返回,不管使用了多少级别的链接。
下面的示例尝试访问john住宅属性的address属性的街道属性。这里使用了两种可选链接,通过住宅和地址属性链接,这两种属性都是可选类型:
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."
使用可选返回值链接方法
前面的示例展示了如何通过可选链接检索可选类型属性的值。您还可以使用可选链接来调用返回可选类型值的方法,并在需要时对该方法的返回值进行链接。
下面的示例通过可选链接调用Address类的buildingIdentifier()方法。此方法返回字符串类型的值?。如上所述,这个方法调用在可选链接之后的最终返回类型也是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()方法本身。
错误处理
错误处理是对程序中的错误条件进行响应和恢复的过程。Swift为在运行时抛出、捕获、传播和操作可恢复错误提供了一流的支持。
有些操作不能保证总是完成执行或产生有用的输出。Optionals用于表示值的缺失,但当操作失败时,了解导致失败的原因通常很有用,这样您的代码就可以相应地做出响应。
例如,考虑从磁盘上的文件中读取和处理数据的任务。这个任务失败的方式有很多种,包括指定路径上不存在的文件、没有读取权限的文件或没有以兼容格式编码的文件。区分这些不同的情况允许程序解决一些错误,并与用户通信任何它不能解决的错误。
Swift中的错误处理与使用Cocoa和Objective-C中的NSError类的错误处理模式交互。
表示和抛出错误
在Swift中,错误由符合错误协议的类型的值表示。这个空协议表示类型可以用于错误处理。
Swift枚举特别适合于建模一组相关的错误条件,这些条件具有相关的值,允许关于错误性质的附加信息进行通信。例如,下面是如何表示在游戏中操作自动售货机的错误条件:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
抛出错误可以让您指示发生了意外事件,而正常的执行流不能继续。使用抛出语句抛出错误。例如,下面的代码抛出一个错误,表明自动售货机需要额外的5个硬币:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
处理错误
当抛出错误时,周围的一些代码必须负责处理错误——例如,通过纠正问题、尝试另一种方法或通知用户失败。
在Swift中有四种处理错误的方法。您可以将错误从一个函数传播到调用该函数的代码,使用do-catch语句处理错误,将错误作为可选值处理,或断言错误不会发生。下面一节将描述每种方法。
当一个函数抛出一个错误时,它会改变程序的流,因此您必须快速地识别代码中可能抛出错误的位置。要在代码中识别这些位置,请编写try关键字或try?或者尝试!变量——在调用可能抛出错误的函数、方法或初始化器的代码段之前。这些关键字将在下面的部分中描述。
Swift中的错误处理类似于其他语言中的异常处理,使用try、catch和throw关键字。与许多语言中的异常处理(包括Swift中的objective - c错误处理)不同的是,它不涉及解除调用堆栈,这个过程的计算代价可能非常昂贵。因此,抛出语句的性能特征与返回语句的性能特征相当。
使用抛出函数传播错误
为了表明函数、方法或初始化器可能抛出错误,可以在函数的声明中在其参数之后写入throw关键字。用抛出标记的函数称为抛出函数。如果函数指定了返回类型,则在返回箭头(->)之前写入throw关键字。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
抛出函数将抛出的错误传播到调用它的作用域。
只有抛出函数才能传播错误。在非抛出函数中抛出的任何错误都必须在函数中处理。
下面的示例中,VendingMachine类有一个vend(itemNamed:)方法,如果所请求的项不可用、缺货或成本超过当前存储金额,那么该方法会抛出一个相应的VendingMachineError。
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing \(name)")
}
}
vend(itemNamed:)方法的实现使用保护语句提前退出方法,如果没有满足购买零食的任何要求,就抛出适当的错误。因为抛出语句会立即转移程序控制,所以只有在满足了所有这些要求之后,才会出售项目。
因为vend(itemNamed:)方法传播它抛出的任何错误,所以调用该方法的任何代码都必须处理错误——使用do-catch语句,try?,或者试试!或者继续传播它们。例如,在下面的示例中,buyFavoriteSnack(person:vendingMachine:)也是一个抛出函数,vend(itemNamed:)方法抛出的任何错误都会传播到调用buyFavoriteSnack(person:vendingMachine:)函数的地方。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
在本例中,buyFavoriteSnack(person: vendingMachine:)函数查找给定用户最喜欢的零食,并通过调用vend(itemNamed:)方法为他们购买。因为vend(itemNamed:)方法会抛出一个错误,所以使用前面的try关键字来调用它。
抛出初始化器可以以与抛出函数相同的方式传播错误。例如,下面清单中购买小吃结构的初始化器调用一个抛出函数作为初始化过程的一部分,它通过将其传播到调用者来处理遇到的任何错误。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
使用Do-Catch处理错误
通过运行代码块,可以使用do-catch语句来处理错误。如果do子句中的代码抛出了错误,它将与catch子句匹配,以确定哪一个可以处理错误。
下面是一个do-catch语句的一般形式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}
您可以在catch之后编写一个模式,以指示子句可以处理哪些错误。如果catch子句没有模式,则该子句匹配任何错误,并将错误绑定到名为error的本地常量。有关模式匹配的更多信息,请参阅模式。
例如,下面的代码与所有三种情况下的VendingMachineError枚举匹配。
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
在上面的示例中,在try表达式中调用buyFavoriteSnack(person:vendingMachine:)函数,因为它会抛出一个错误。如果抛出错误,则立即将执行转移到catch子句,该子句决定是否允许继续传播。如果没有匹配模式,错误将被最终catch子句捕获,并被绑定到一个本地错误常量。如果没有抛出错误,则执行do语句中的其余语句。
catch子句不必处理do子句中的代码可能抛出的所有错误。如果没有catch子句处理错误,错误将传播到周围的范围。但是,传播的错误必须由周围的某个范围处理。在非抛出函数中,包含do-catch的子句必须处理错误。在抛出函数中,要么包含do-catch子句,要么调用方必须处理错误。如果错误传播到顶级范围而不被处理,您将得到一个运行时错误。
例如,可以编写上面的示例,以便调用函数捕获任何不是VendingMachineError的错误:
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Invalid selection, out of stock, or not enough money.")
}
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Invalid selection, out of stock, or not enough money."
在滋养(with:)函数中,如果vend(itemNamed:)抛出一个错误,该错误属于VendingMachineError枚举的一种情况,滋养(with:)通过打印消息处理错误。否则,滋养(with:)将错误传播到其调用站点。然后,通用catch子句捕获错误。
将错误转换为可选值
你用试试吗?通过将错误转换为可选值来处理错误。如果在评估try时抛出错误?表达式,表达式的值为nil。例如,在以下代码中x和y具有相同的值和行为:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
如果someThrowingFunction()抛出错误,x和y的值为nil。否则,x和y的值就是函数返回的值。注意,x和y是someThrowingFunction()返回的任何类型的可选值。这里函数返回一个整数,因此x和y是可选的整数。
用试一试吗?当您希望以相同的方式处理所有错误时,允许您编写简洁的错误处理代码。例如,下面的代码使用几种方法来获取数据,如果所有方法都失败,则返回nil。
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
禁用错误传播
有时你知道抛出函数或方法不会在运行时抛出错误。在这种情况下,你可以试试写!在表达式禁用错误传播并将调用包装在运行时断言中,断言不会抛出错误。如果实际抛出了一个错误,您将得到一个运行时错误。
例如,下面的代码使用loadImage(atPath:)函数,它在给定的路径加载图像资源,或者在无法加载图像时抛出错误。在这种情况下,由于映像是随应用程序一起提供的,因此不会在运行时抛出错误,因此禁用错误传播是合适的。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定清理行动
在代码执行离开当前代码块之前,可以使用defer语句来执行一组语句。这个语句允许您执行任何必须执行的清理操作,而不管执行是如何离开当前代码块的——无论是由于抛出错误还是由于返回或中断之类的语句——都是如此。例如,您可以使用defer语句来确保关闭了文件描述符并释放了手动分配的内存。
延迟语句将延迟执行,直到退出当前范围。该语句由defer关键字和随后执行的语句组成。延迟语句可能不包含任何会将控制从语句转移出去的代码,例如break或return语句,或者通过抛出错误。延迟操作的执行顺序与在源代码中写入的顺序相反。也就是说,第一个延迟语句中的代码执行到最后,第二个延迟语句中的代码执行到倒数第二,依此类推。源代码顺序中的最后一个延迟语句首先执行。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}
上面的示例使用了一个defer语句,以确保open(_:)函数有一个相应的调用来关闭(_:)
即使不涉及错误处理代码,也可以使用延迟语句。