错误处理(Error Handling)

469 阅读10分钟

Error Handling是在项目中响应和从错误状态中恢复的过程。swift在运行期为抛出,捕捉,传递和操作恢复错误提供一流的支持。

一些操作不能保证通常完成执行后者得到有用的输出。可选类型用来表示一个值缺失,但是当可选类型失败的时候,经常有助于理解什么引起了错误,所以你的代码可以合理的响应。

例如,思考一个从磁盘上的文件中读取和处理数据的任务。有很多种这个任务失败的方式,包括在指定的路径上不存在文件,文件没有读权限,或者文件没有用合适的格式编码。区别这些不同的情况使程序解决一些错误并且和用户沟通他解决不了的错误。

swift中的错误处理和Cocoa与Objective-C的NSError类使用的错误处理模式交互。更多关于这个类的信息,查看Handling Cocoa Errors in Swift

表示和抛出错误(Representing and Throwing Errors)

在swift中,错误用遵守Error协议的类型的值表示。这个简单的协议指定为错误处理使用的类型。

swift枚举非常适合模型化一组相关的错误情况,使用关联值可以交流关于错误性质的其他信息。例如,这是你如何表示在游戏中一个贩卖机的错误情况:

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

通过抛出一个错误指出不希望发生的事并且正常的执行流程不能继续。使用throw语句来抛出一个错误。例如,下面的代码抛出一个错误来指明贩卖机需要额外的物美硬币:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

处理错误(Handling Errors)

当抛出了错误的时候,一些周围的代码块必须对处理错误作出响应--例如,在修正问题之前,尝试另一个方法,或者通知使用者错误。

在swift中有四种方式处理错误。可以把错误从函数传递给调用该函数的代码,使用do-catch处理错误,把错误当做可选值处理,或者断言错误不会发生。每一个方式在下面的章节中介绍。

当一个函数抛出一个错误,它改变了你程序的流,所以快速的定位抛出错误的代码的位置非常重要。要在代码中定位这些位置,写try关键字--或者try?或者try!变量--在调用能抛出错误的函数,方法,或者初始化器的代码之前。这些关键字的描述在后面章节。

swift中的错误处理类似其他语言中的异常处理,使用try,catch,和throw关键字。不想其他语言中的异常处理--包括Objective-C--swift中的错误处理不包含循环调用栈,一个计算高昂的过程。因此抛出语句的性能特征和返回语句的性能特征差不多。

使用抛出函数传递错误(Propagating Errors Using Throwing Functions)

指明一个函数,方法或者初始化器可以抛出错误,在函数声明中的参数后面写throws关键字。标记了throw是的函数成为throwing function。如果函数指定了范慧慧类型,在返回箭头(->)之前写throws关键字。

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

一个抛出函数把它自己内部抛出的错误传递到调用它的区域。

只有抛出函数可以传递错误。任何在非抛出函数中的错误必需在函数内部处理。

下面的例子,VendingMaching类有一个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:)方法传递任何它throws的错误,任何调用这个方法的代码必须处理这个错误--使用do-catch语句,try?,或者try!--或者继续传递他们。例如,下面例子中的buyFovoriteSnack(Person:VendingMaching:)也是一个抛出函数,任何vend(itemNamed:)方法抛出的错误都会向上传递到buyFavoriteSnack(person:vendingMaching:)方法调用的地方。

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

在这个例子中,函数bugFavoriteSnack(person:vendingMachine:)查找一个给定的人的最喜欢的小吃并且尝试通过调用vend(itemNamed:)方法来给他们买它。因为vend(itemNamed:)方法可以抛出错误,在他之前用try关键字来调用他。

抛出初始化器可以用和抛出函数一样的方式来传递错误。例如,下面列表的PurchasedSnack结构体的初始化器在初始化过程中调用一个抛出函数,并且他讲他遇到的任何错误通过把它们传递到它的调用者来处理这些错误。

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}

使用Do-Catch处理错误(Handling Errors Using 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的常量。更多关于模式匹配的信息,查看Patterns

例如,下面的代码匹配枚举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:VendingMaching:)在一个try表达式中调用,因为他可以抛出错误。如果抛出了错误,执行立刻切到catch子句,决定是否允许传递继续。如果没有模式匹配到,错误被最后的catch子句捕获并且绑定到本地error常量。如果没有错误抛出,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."

在nourish(with:)函数中,如果vend(itemNamed:)抛出一个VendingMachineError枚举中的一个情况的错误,nourish(with:)通过打印一条消息来处理错误。否则,nourish(with:)把错误传递到它的调用的地方。之后错误被tong普通的catch子句捕获。

把错误转换为可选类型值(Converting Errors to Optioal Values)

使用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和y的值时nil。否则,x和y的值时函数返回的值。注意,xhey是一个someThrowingFunction()返回的任何类型的可选类型。这里函数返回了一个整型,所以x和y是可选的整型。

使用Try?使你当想要用一样的方式处理全部错误的时候用简明的错误处理代码。例如,下面的代码使用多个方式来获取数据,或者如果全部的方式失败了返回nil。

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

禁止错误传递(Disabling Error Propagation)

有时候你知道一个抛出函数或者方法实际上不会在运行期抛出错误。在这些情况下,可以在表达式钱使用try!来截至错误传递并且在把调用封装在运行时的没有错误抛出的断言中。如果实际上抛出了错误,你将会得到一个运行时错误。

例如,下面的代码使用一个loadImage(atPath:)函数,导入给定路径的图片资源后者如果图片不能导入的话抛出一个错误。这个情况中,因为图片用application提供,在运行时不会抛出错误,所以他适合禁用错误传递。

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

指定清理行动(Specifying Cleanup Actions)

使用defer语句来在代码执行离开现在代码块之前执行大量代码。这个语句使你做任何必需的无论执行如何离开当前代码块都应该执行的清理--是因为抛出错误或者因为像return或者break的语句离开。例如,你可以使用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(_:)函数对close(_:)有一个对应的的调用。

甚至在没有包含错误处理语句的时候可以使用延迟语句。