Swift错误处理

目录

引入

错误处理是对程序中的错误做出响应并从中恢复的过程。 Swift为运行时抛出、 捕获、 传播和处理可恢复错误提供了一流的支持。

有些操作并不能保证始终完成执行或产生有用的输出。 可选项用于表示没有值, 但当操作失败时, 了解导致失败的原因通常很有用, 这样你的代码就能做出相应的响应。

表示和抛出错误

在Swift中, 错误由符合 Error 协议的类型值表示。 这种空协议表示一种类型可用于错误处理。

Swift枚举尤其适用于对一组相关的错误条件进行建模, 相关值允许传递有关错误性质的附加信息。

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

通过抛出错误, 可以说明发生了意外情况, 正常的执行流程无法继续。 可以使用抛出语句来抛出错误。

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

处理错误

当抛出错误时, 周围的代码必须负责处理错误。

在Swift中, 有四种处理错误的方法。 你可以将错误从函数传播到调用该函数的代码, 使用 do-catch 语句处理错误, 将错误作为可选值处理, 或者断言错误不会发生。

当函数抛出错误时, 会改变程序的流程, 因此快速识别代码中可能抛出错误的地方非常重要。 要识别代码中的这些地方, 在调用可能抛出错误的函数、 方法或初始化器的代码前写入 try 关键字(或 try? )。

使用抛出函数传播错误

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

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

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

下面的示例, 自动售货机有vend方法, 如果请求物品没有现货, 或者没有足够的钱, 就会抛出 VendingMacheError。

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)")
    }
}

下面的 buyFavoriteSnack函数查找指定人喜欢的零食, 尝试通过vend方法完成购买。 因为vend方法可能出错, 所以调用方式前面需要加上 try 关键字。

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)
}

使用 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后面编写规则,确定可以处理的错误。 如果没有规则, 表示处理任何错误, 并将错误绑定到 error 常量。

示例

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."

catch 子句不必处理 do 语句中代码可能抛出的所有错误。 如果没有一个 catch 子句处理错误, 错误就会传播到周围的作用域。 但是传播的错误必须由某个周围作用域处理。 在非抛出函数中, 外层 do-catch 语句必须处理该错误。 在抛出函数中, 外层 do-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.")
    }
}

将错误转化成可选值

使用 try? 可以将错误转换为可选值, 从而处理错误。 如果在评估 try? 表达式时出错, 表达式的值将为 nil。

func someThrowingFunction() throws -> Int {
    // ...
}
let x = try? someThrowingFunction()

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

想以同样的方式处理所有错误时, 使用 try?可以编写简洁的错误处理代码。

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

禁用错误传播

有时知道一个抛出函数或方法实际上不会在运行时抛出错误。 在这种情况下, 你可以在表达式前写入 try! 以禁用错误传播, 并在运行时断言不会抛出错误的情况下进行调用。 如果确实发生了错误, 则会出现运行时错误。

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

指定错误类型

上述所有示例都使用了最常见的错误处理方式, 即代码抛出的错误可以是符合错误协议的任何类型的值。 这种方法符合实际情况, 即你不可能提前知道代码运行时可能发生的所有错误, 尤其是在传播其他地方 抛出的错误时。 它还反映了一个事实, 即错误会随着时间的推移而改变。 新版本的库,包括你的依赖 库使用的库, 可能会产生新的错误, 而现实世界中用户配置的复杂性可能会暴露出开发或测试过程中 不可见的故障模式。 上述示例中的错误处理代码总是包含一个默认情况, 用于处理没有特定 catch 子句的错误。

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

  • 在不支持动态分配内存的嵌入式系统上运行代码时。
  • 当错误是某个代码单元(如库)的实现细节,而不是该代码接口的一部分时。
  • 在只传播由通用参数描述的错误的代码中, 例如一个接收闭包参数的函数, 会传播来自该闭包的任何错误。

有这样一个错误类型。

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

要指定函数只抛出 StatisticsError 值作为其错误, 可以在声明函数时写入 throws(StatisticsError), 而不是只写入 throws。 这种语法也称为类型化抛出, 因为在声明中的 throws 后写入了错误类型。

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]!)
}

除了指定函数的错误类型外, 还可以为 do-catch 语句编写特定类型错误。

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"

指定清理操作

使用延迟语句可以在代码执行离开当前代码块之前执行一组语句。 无论代码执行以何种方式离开当前代 码块—是由于抛出错误还是由于 return 或 break 等语句—该语句都可以让你执行任何必要的清理 工作。

延迟语句延迟执行,直到退出当前作用域。 该语句由 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.
    }
}

上例使用了延迟语句, 以确保 open 函数有相应的 close 调用。