Skip to content

错误处理

错误处理是响应和从程序中的错误条件中恢复的过程。Swift 提供了一级支持,可以在运行时抛出、捕获、传播和处理可恢复的错误。

有些操作不能保证总是完成执行或产生有用的结果。可选值用于表示不存在的值,但当操作失败时,了解失败的原因通常是有用的,这样代码可以相应地做出响应。

例如,考虑从磁盘上的文件读取和处理数据的任务。这个任务可能会以多种方式失败,包括文件在指定路径不存在,文件没有读取权限,或者文件没有以兼容的格式编码。区分这些不同的情况可以让程序解决一些错误,并向用户传达它无法解决的任何错误。

注意
在 Swift 中,错误处理与使用 NSError 类的 Cocoa 和 Objective-C 中的错误处理模式相互操作。有关此类的更多信息,请参阅在 Swift 中处理 Cocoa 错误。

表示和抛出错误

在 Swift 中,错误由遵循 Error 协议的值类型来表示。这个空协议表示一种类型可以用于错误处理。

Swift 枚举特别适合于建模一组相关的错误条件,附加值允许传达有关错误的性质的额外信息。例如,以下是如何在游戏内表示自动售货机操作的错误条件的方法:

swift
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

抛出错误可以表示发生了意料之外的事情,正常的执行流程无法继续。您使用 throw 语句来抛出错误。例如,以下代码抛出错误以表示自动售货机需要五枚额外的硬币:

swift
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

错误处理

当抛出错误时,必须由周围的代码段负责处理错误——例如,通过纠正问题、尝试其他方法或通知用户失败。

在 Swift 中处理错误有四种方式。你可以从一个函数将错误传递给调用该函数的代码,使用 do-catch 语句处理错误,将错误作为可选值处理,或者断言错误不会发生。每种方法在下面的部分中描述。

当一个函数抛出错误时,它会改变程序的流程,因此重要的是能够快速识别代码中可能抛出错误的地方。要在代码中识别这些地方,可以在调用可能抛出错误的函数、方法或初始化器之前写入 try 关键字——或 try?try! 变体。这些关键字在下面的部分中描述。

注意
在 Swift 中,错误处理类似于其他语言中的异常处理,使用 trycatchthrow 关键字。与 Objective-C 等许多语言中的异常处理不同,Swift 中的错误处理不会涉及展开调用堆栈的过程,这是一个计算成本较高的过程。因此,throw 语句的性能特征与 return 语句相当。

使用抛错函数传播错误

要表示一个函数、方法或初始化器可以抛出错误,您需要在函数声明中的参数之后写入 throws 关键字。标记为 throws 的函数称为抛错函数。如果函数指定了返回类型,您需要在返回箭头(->)之前写入 throws 关键字。

swift
func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

抛错函数会将内部抛出的错误传播到调用它的作用域。

注意
只有抛错函数才能传播错误。非抛错函数内部抛出的任何错误都必须在函数内部处理。

在下面的示例中,VendingMachine 类有一个 vend(itemNamed:) 方法,如果请求的项目不可用、缺货或成本超过当前存入金额,则该方法会抛出适当的 VendingMachineError

swift
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:) 也是一个抛出函数,而 vend(itemNamed:) 方法抛出的任何错误将传播到调用 buyFavoriteSnack(person:vendingMachine:) 函数的点。

swift
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 结构的初始化器在初始化过程中会调用一个抛函数,并且它会通过将错误传递给其调用者来处理遇到的任何错误。

swift
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 语句的一般形式:

swift
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 枚举的三种情况。

swift
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 子句,这些子句决定是否允许错误传播继续。如果没有匹配到模式,错误会被最终的 catch 子句捕获并绑定到一个局部 error 常量。如果没有抛出错误,do 语句中的剩余语句将被执行。

catch 条款不必处理 do 条款中代码可能抛出的每一种可能的错误。如果 catch 条款都没有处理该错误,该错误将传播到外围作用域。但是,传播的错误必须由某个外围作用域处理。在非抛出函数中,必须由包含的 do-catch 语句处理该错误。在抛出函数中,必须由包含的 do-catch 语句或调用者处理该错误。如果错误传播到顶层作用域而未被处理,将会引发运行时错误。

例如,上述示例可以重写为:任何不是 VendingMachineError 的错误将由调用函数捕获。

swift
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:) 将错误传递给其调用位置。然后该错误会被通用的 catch 子句捕获。

另一种捕获多个相关错误的方法是在 catch 之后列出它们,并用逗号分隔。例如:

swift
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。例如,在以下代码中,xy 具有相同的值和行为。

swift
func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

如果 someThrowingFunction() 抛出错误,xy 的值为 nil。否则,xy 的值为函数返回的值。请注意,xysomeThrowingFunction() 返回类型的可选值。此处函数返回整数,因此 xy 是可选整数。

使用 try? 可以在您希望以相同方式处理所有错误时编写简洁的错误处理代码。例如,以下代码使用了几种方法来获取数据,或者如果所有方法都失败,则返回 nil

swift
func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

禁用错误传播

有时你知道一个抛错函数或方法在运行时实际上不会抛出错误。在这种情况下,你可以在表达式前写上 try! 来禁用错误传播,并将调用包裹在一个运行时断言中,以确保不会抛出错误。如果确实抛出了错误,你将在运行时得到一个错误。

例如,以下代码使用了一个 loadImage(atPath:) 函数,该函数根据给定路径加载图像资源,如果无法加载图像则抛出错误。在这种情况下,因为图像与应用程序一起打包,所以在运行时不会抛出错误,因此禁用错误传播是合适的。

swift
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

指定错误类型

上面的所有示例都使用了最常见的错误处理方式,其中代码抛出的错误可以是任何符合 Error 协议的类型。这种方法符合实际情况,即在代码运行时你无法提前知道所有可能发生的错误,尤其是在传递来自其他地方抛出的错误时。这也反映了错误会随时间变化的事实。新版本的库——包括你依赖的库——可能会抛出新的错误,而真实世界用户配置的复杂性可能会暴露在开发或测试期间未发现的失败模式。上面示例中的错误处理代码始终包含一个默认情况来处理没有特定 catch 子句的错误。

大多数 Swift 代码不会指定它抛出的错误类型。然而,在以下特殊情况下,你可能会限制代码只抛出一种特定类型的错误:

  • 当在不支持动态分配内存的嵌入式系统上运行代码时,抛出 any Error 或另一个封装协议类型的实例需要在运行时分配内存以存储错误。相比之下,抛出特定类型的错误可以让 Swift 避免为错误进行堆分配。
  • 当错误是某些代码单元(如库)的实现细节,而不是该代码的接口的一部分时。由于这些错误仅来自库,而不是其他依赖项或库的客户端,因此可以列出所有可能的失败情况。并且由于这些错误是库的实现细节,因此它们总是在库内部处理。
  • 在只传播由通用参数描述的错误的代码中,例如一个接受闭包参数并从该闭包传播任何错误的函数。有关传播特定错误类型和使用 rethrows 之间的比较,请参见重新抛出函数和方法。

例如,考虑汇总评分的代码,并使用以下错误类型:

swift
enum StatisticsError: Error {
    case noRatings
    case invalidRating(Int)
}

为了指定一个函数只抛出 StatisticsError 值作为其错误,你可以在声明函数时写 throws(StatisticsError) 而不是只写 throws。这种语法也称为类型抛出,因为在声明中你将错误类型写在 throws 之后。例如,下面的函数抛出 StatisticsError 值作为其错误。

swift
func summarize(_ ratings: [Int]) throws(StatisticsError) {
    guard !ratings.isEmpty else { throw .noRatings }

    var counts = [1: 0, 2: 0, 3: 0]
    for rating in ratings {
        guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
        counts[rating]! += 1
    }

    print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}

在上面的代码中,summarize(_:) 函数总结了一组在 1 到 3 的评分列表。如果输入无效,该函数会抛出一个 StatisticsError 实例。上面代码中两次抛出错误的地方省略了错误类型的说明,因为函数的错误类型已经定义好了。在这种函数中抛出错误时,可以使用简短形式 throw .noRatings 而不是写 throw StatisticsError.noRatings

当你在函数开头写一个特定的错误类型时,Swift 会检查你是否抛出了其他错误。例如,如果你在上面的 summarize(_:) 函数中尝试使用本章前面示例中的 VendingMachineError,编译时会生成一个错误。

你可以在一个常规的抛出函数中调用一个使用类型抛出的函数:

swift
func someThrowingFunction() throws {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

上面的代码没有为 someThrowingFunction() 指定错误类型,所以它抛出 any Error。你也可以显式地写错误类型为 throws(any Error);下面的代码与上面的代码等效:

swift
func someThrowingFunction() throws(any Error) {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

在这段代码中,someThrowingFunction() 会传播 summarize(_:) 投掷的任何错误。summarize(_:) 投掷的错误总是 StatisticsError 值,这也是一种有效的错误类型,可以由 someThrowingFunction() 投掷。

就像你可以写一个永远不会返回的函数,返回类型为 Never 一样,你也可以写一个永远不会抛出错误的 throws(Never) 函数:

swift
func nonThrowingFunction() throws(Never) {
  // ...
}

这个函数不能抛出错误,因为不可能创建一个类型为 Never 的值来抛出。

除了指定函数的错误类型外,你还可以为 do-catch 语句指定一个特定的错误类型。例如:

swift
let ratings = []
do throws(StatisticsError) {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available"

在这段代码中,do throws(StatisticsError) 表示 do-catch 语句抛出 StatisticsError 类型的错误。和其他 do-catch 语句一样,catch 子句可以捕获所有可能的错误,或者将未处理的错误传递给外部作用域处理。这段代码捕获了所有错误,使用了 switch 语句,为每个枚举值定义了一个单独的 case。和其他没有模式的 catch 子句一样,该子句匹配任何错误并将错误绑定到名为 error 的局部常量。由于 do-catch 语句抛出 StatisticsError 类型的值,error 是一个 StatisticsError 类型的值。

上面的 catch 子句使用了 switch 语句来匹配和处理每种可能的错误。如果你在 StatisticsError 中添加了一个新的 case 但没有更新错误处理代码,Swift 会报错,因为 switch 语句将不再是详尽的。对于一个捕获自身所有错误的库,你可以使用这种方法来确保任何新的错误都有相应的新的代码来处理它们。

如果一个函数或 do 块只抛出一种类型的错误,Swift 会推断出这段代码使用了带类型的抛出。使用这种更短的语法,你可以将上面的 do-catch 示例写成如下形式:

swift
let ratings = []
do {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available"

尽管上面的 do-catch 块没有指定它抛出什么类型的错误,Swift 会推断它抛出 StatisticsError。你可以显式地写 throws(any Error) 以避免让 Swift 推断带类型抛出。

指定清理动作

你使用 defer 语句在代码执行离开当前代码块之前执行一组语句。该语句让你可以在代码执行离开当前代码块之前执行任何必要的清理操作,无论代码是由于错误抛出还是由于诸如 returnbreak 的语句而离开当前代码块。例如,你可以使用 defer 语句确保文件描述符被关闭并且手动分配的内存被释放。

A defer 语句推迟执行直到当前作用域退出。该语句由 defer 关键字和稍后要执行的语句组成。推迟执行的语句不能包含任何会将控制转移到语句之外的代码,例如 breakreturn 语句,或者抛出错误。推迟的操作按与您源代码中相反的顺序执行。也就是说,defer 语句中的代码最后执行,defer 语句中的代码倒数第二执行,依此类推。源代码中最后的 defer 语句最先执行。

swift
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 语句。更多信息,请参见延迟操作。