Swift5.4 语言指南(十九) 错误处理
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/)
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9739443.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
错误处理是对程序中的错误情况做出响应并从中恢复的过程。Swift在运行时为抛出,捕获,传播和操作可恢复错误提供了一流的支持。
某些操作不能保证总是完成执行或产生有用的输出。可选变量用于表示缺少值,但是当操作失败时,了解导致失败的原因通常很有用,以便您的代码可以做出相应的响应。
例如,考虑从磁盘上的文件读取和处理数据的任务。有多种方法可以使此任务失败,包括指定路径中不存在的文件,没有读取权限的文件或未以兼容格式编码的文件。在这些不同情况之间进行区分可以使程序解决一些错误,并向用户传达它无法解决的任何错误。
笔记
Swift中的错误处理与使用NSError
Cocoa和Objective-C中的类的错误处理模式进行互操作。有关此类的更多信息,请参见在Swift中处理可可错误。
表示和抛出错误
在Swift中,错误由符合Error
协议的类型的值表示。此空协议表示可以将类型用于错误处理。
Swift枚举特别适合于对一组相关的错误条件进行建模,其关联值允许传达有关错误性质的其他信息。例如,这是在游戏内操作自动售货机的错误情况的表达方式:
- enum VendingMachineError: Error {
- case invalidSelection
- case insufficientFunds(coinsNeeded: Int)
- case outOfStock
- }
引发错误可让您指示发生了意外情况,并且正常的执行流程无法继续进行。您使用一条throw
语句引发错误。例如,以下代码引发错误,以指示自动售货机需要另外五个硬币:
- throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
处理错误
引发错误时,周围的某些代码段必须负责处理错误,例如,通过更正问题,尝试其他方法或将故障通知用户。
Swift中有四种处理错误的方法。您可以将错误从函数传播到调用该函数的代码,使用do
-catch
语句处理错误,将错误作为可选值处理,或断言不会发生错误。每种方法在下面的部分中进行介绍。
当一个函数抛出错误时,它会改变程序的流程,因此,重要的是,您必须快速识别代码中可能引发错误的位置。要在代码中标识这些位置,请在一段调用可能会引发错误的函数,方法或初始化程序的代码之前,写上try
关键字(或thetry?
或try!
variant)。这些关键字在以下各节中进行介绍。
笔记
错误迅速处理类似的异常处理在其他语言中,使用了的try
,catch
和throw
关键字。与许多语言(包括Objective-C)中的异常处理不同,Swift中的错误处理并不涉及展开调用堆栈,该过程在计算上可能会非常昂贵。这样,throw
语句的性能特征可与语句的性能特征相媲美return
。
使用投掷函数传播错误
为了表明函数,方法或初始化程序可能引发错误,请throws
在函数的参数后的声明中写入关键字。标throws
有的功能称为投掷功能。如果函数指定了返回类型,则将throws
关键字写在返回箭头(->
)之前。
- func canThrowErrors() throws -> String
- func cannotThrowErrors() -> String
抛出函数会将抛出的错误传播到调用它的作用域中。
笔记
只有throwing函数可以传播错误。抛出于非抛出函数内部的任何错误都必须在函数内部进行处理。
在下面的示例中,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:)
方法的实现使用guard
语句来提前退出该方法,如果不满足购买零食的任何要求,则抛出适当的错误。因为一条throw
语句会立即转移程序控制权,所以只有在满足所有这些要求的情况下,才可以出售商品。
因为该vend(itemNamed:)
方法传播了它抛出的任何错误,所以任何调用此方法的代码都必须使用do
-catch
语句try?
,或try!
-处理错误,或者继续传播它们。例如,buyFavoriteSnack(person:vendingMachine:)
在下面的示例中,也是一个throwing函数,该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
引发初始化器可以以与引发函数相同的方式传播错误。例如,PurchasedSnack
下面清单中的结构的初始化程序在初始化过程中调用了throwing函数,并通过将其传播给调用方来处理遇到的任何错误。
- 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 pattern 3, pattern 4 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."
在上面的示例中,该buyFavoriteSnack(person:vendingMachine:)
函数在try
表达式中被调用,因为它可能引发错误。如果抛出错误,执行将立即转移到这些catch
子句,这些子句决定是否允许继续传播。如果没有匹配的模式,则该错误将被finalcatch
子句捕获,并绑定到局部error
常量。如果没有引发错误,do
则执行该语句中的其余语句。
该catch
条款没有处理每一个可能的错误,该代码do
子句可以抛出。如果没有任何一个catch
子句处理该错误,则该错误会传播到周围的范围。然而,传播错误必须处理一些周边范围。在非抛出函数中,必须使用do
-catch
语句处理该错误。在throwing函数中,封闭的do
-catch
语句或调用者必须处理该错误。如果错误未得到处理就传播到顶级范围,则会出现运行时错误。
例如,可以编写上面的示例,以便VendingMachineError
调用函数捕获不是a的任何错误:
- func nourish(with item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch is VendingMachineError {
- print("Couldn't buy that from the vending machine.")
- }
- }
- do {
- try nourish(with: "Beet-Flavored Chips")
- } catch {
- print("Unexpected non-vending-machine-related error: \(error)")
- }
- // Prints "Couldn't buy that from the vending machine."
在该nourish(with:)
函数中,如果vend(itemNamed:)
引发VendingMachineError
枚举情况之一nourish(with:)
的错误,则通过打印消息来处理该错误。否则,nourish(with:)
将错误传播到其呼叫站点。然后,该错误将被generalcatch
子句捕获。
捕获几个相关错误的另一种方法是将它们列出在后面catch
,并用逗号分隔。例如:
- func eat(item: String) throws {
- do {
- try vendingMachine.vend(itemNamed: item)
- } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
- print("Invalid selection, out of stock, or not enough money.")
- }
- }
该eat(item:)
功能列出了要捕获的自动售货机错误,其错误文本与该列表中的项目相对应。如果抛出了列出的三个错误中的任何一个,则此catch
子句通过打印消息来处理它们。其他任何错误都会传播到周围的范围,包括以后可能会添加的自动售货机错误。
将错误转换为可选值
您可以try?
通过将错误转换为可选值来处理错误。如果在评估try?
表达式时抛出错误,则表达式的值为nil
。例如,在以下代码中x
,y
它们具有相同的值和行为:
- func someThrowingFunction() throws -> Int {
- // ...
- }
- let x = try? someThrowingFunction()
- let y: Int?
- do {
- y = try someThrowingFunction()
- } catch {
- y = nil
- }
如果someThrowingFunction()
抛出错误,则x
and的y
值为nil
。否则,x
andy
的值就是函数返回的值。请注意,x
和y
是someThrowingFunction()
返回的可选类型。这里的函数返回一个整数,所以x
和y
是可选的整数。
try?
当您要以相同方式处理所有错误时,使用using可以编写简洁的错误处理代码。例如,以下代码使用几种方法来获取数据,或者nil
如果所有方法均失败,则返回。
- func fetchData() -> Data? {
- if let data = try? fetchDataFromDisk() { return data }
- if let data = try? fetchDataFromServer() { return data }
- return nil
- }
禁用错误传播
有时,您知道抛出函数或方法实际上不会在运行时抛出错误。在这种情况下,您可以try!
在表达式之前编写以禁用错误传播,并将调用包装在不会引发任何错误的运行时断言中。如果实际抛出错误,您将收到运行时错误。
例如,以下代码使用一个loadImage(atPath:)
函数,该函数在给定路径下加载图像资源,或者在无法加载图像时抛出错误。在这种情况下,由于该映像是与应用程序一起提供的,因此在运行时不会引发任何错误,因此应该禁用错误传播。
- let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
指定清理措施
您可以defer
在代码执行离开当前代码块之前使用一条语句来执行一组语句。通过该语句,无论执行如何离开当前代码块,无论是由于引发错误还是由于诸如return
或之类的语句而离开,您都可以执行该操作break
。例如,您可以使用一条defer
语句来确保关闭文件描述符并释放手动分配的内存。
一条defer
语句将延迟执行,直到退出当前作用域。该语句由defer
关键字和以后要执行的语句组成。延迟的语句可能不包含任何将控制权移出该语句的代码,例如abreak
或一条return
语句,或引发错误。延迟的操作以与您在源代码中写入的顺序相反的顺序执行。也就是说,第一个defer
语句中的代码最后执行,第二个defer
语句中的代码倒数第二执行,依此类推。defer
源代码顺序中的最后一条语句首先执行。
- 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(_:)
函数具有对的相应调用close(_:)
。
笔记
defer
即使不涉及任何错误处理代码,也可以使用语句。