错误管理是对我们的程序中产生的错误进行的一种响应和恢复的过程。swift语言支持在runtime的时候 抛出(throwing),获取(catching),传播(propagating),和操作(manipulating)可恢复的错误。
一些代码的执行不能完全保证在执行完毕之后可以产出有用的输出。可选类型通常是用来表达某个值的缺失。当某次执行失败的时候。让我们来理解对引起失败的原因是很有帮助的,所以在我们的代码中要进行对应的响应。
举个例子,当我们从一个磁盘中的某个文件进行读取和处理数据的时候,有很多中方式这个读取和处理的工作会失败。包括这个文件不在某个指定的路径中。或者是我们对这个文件没有读取权限。再或者是这个文件在兼容模式下没有加密。区分这些不同情况允许程序解决某些错误,并为用户提供任何无法解决的错误。
在swift里面error用符合Error
协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。
枚举比较适合构建一组相关的错误条件,关联值可以为我们提供于某个错误信息所关联的额外信息,就像(404: 资源不存在)。举个例子下面着是一个在游戏里面操作自动售货机怎么表达当遇到某个错误信息。
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
抛出错误,可以使我们指明在某个执行流中某些意料之外的事情发生。使用Throw
语句来抛出某个错误,下面的案例就是当我们遇到某个错误的时候并进行指明当前收货机需要5个硬币。
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
如果说一个error被抛出了,我们需要在这个抛出的错误的代码附近必须对这个已抛出的代码进行管理响应,就是当遇到这个错误的时候我们应该怎么做,改正这个问题,尝试其他方法,在或者通知用户这个错误,就像上面的例子那样,当自动收货机遇到那个问题的时候,一个附加的信息通知用户需要5个硬币,
在swift语言里面有四种
管理错误的方法,可以将错误从函数传播到调用该函数的代码中。用do-catch
语句来管理这些错误。把错误当作可选类型中的值来管理,或者断言这个错误不会发生,上面的四种方法都将会在下面的章节中详细介绍。
当一个函数抛出某个错误的时候,它会改变整个程序的执行流,所以很重要的是我们要及时对产生的错误进行响应,指明一个位置作为对抛出的错误用上面四种方法对其进行处理。为了标出这些地方,使用关键字try?
或者try!
因为某段代码调用函数,方法,或者构造器会抛出错误,这俩关键字会在下面详细描述。
在函数定义的括号后面写上throws
关键字来指明函数,方法或者构造器会抛出错误。一个标有throws
的函数我们称之为throw函数,如果说这个throw函数指定了返回类型,在关键字throws后面添加->
来指明返回的类型。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
一个throw函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。需要注意的是只有throw函数才会传播错误,其他任何非throw函数抛出的错误,只能在该函数的内部进行处理。
下面这个例子定义的类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 参考 No.5 - 控制转移语句 - 提早退出 章节
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:)
同样也是一个throw函数,也就是说任何被vend(itemNamed:)
方法抛出的错误,当调用buyFavoriteSnack(person:vendingMachine:)函数的时候这些被抛出的错误将会向上传播。
let favoriteSnacks = [
// 人名和喜欢的零食
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
/* ?? 空合运算符 详情参考 No.2 基本运算符 - 空合运算符
a??b a永为可选 a有值强制使用,无值则用b */
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
在这个例子里面buyFavoriteSnack(person: vendingMachine:)
函数会在以给出的人名和喜欢的零食,该函数尝试从该出的信息里面购买这些零食,通过调用vend(itemNamed: )
方法。因为该方法会抛出一个错误。所以在调用该方法的时候在方法名字前加try
关键字
throw
构造器和throw
函数传播错误的方法是一样的。该结构体的构造器调用这个throw函数作为该结构体构造过程的一部分,并且它通过将错误传播到调用方来处理遇到的任何错误。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
使用do-catch来通过运行一个代码块的方式来管理这个错误,如果说这个错误在do子句里面被抛出来,这个do子句会与catch子句来决定那一个catch子句可以管理解决这个被do子句抛出的error。
下面是一个do-catch语句的格式
在这个catch子句的后面写上一个模型(pattern)并且指明哪一个子句来管理那些error。如果说这个某个catch
子句并没有模型,那这个子句将会把匹配到的error封装在一个叫error的本地常量里面。更过有关模型的详见pattern篇章。
下面的代码处理了 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).")
}
// 输出:"Insufficient funds. Please insert an additional 2 coins.
在上面的这个例子中,这个buyFavoriteSnack(person:vendingMachine:)
函数调用在try的表达式中。也就是说如果在这个try表达式中抛出了一个error的话,那么整个执行流将会被转移到这个catch
的里面 对抛出的error进行管理。这时候这个catch子句就会继续执行这个知道最后被最后那个catch字句捕获到,输出上个catch子句所管理的。如果说在try表达式里面没有error被抛出那么久会执行do语句里面剩余的语句,
结合上面实例理解 当try表达式里抛出一个error和没有抛出error这个error是怎么转移和执行的。1. 如果在try表达式中抛出了Out of Stock
这个错误信息,那么该错误就会被catch VendingMachineError.outOfStock { print("Out of Stock.")
语句所执行,最后在结合并加上最后一个catch子句 综合起来输出 这个error信息。也就是Unexpected error: Out of Stock.
。如果在这个try表达式里面没有error被抛出那么就会执行do语句中的非try表达式,也就是print("Success! Yum.")
那么没有抛出error的情况下 就回直接输出 Success!Yum
下面这个例子久挺有意思的。它会分VendingMachineError和非VendingMachineError。在这个nourish(with:)
函数里面,如果说vend(itemNamed:)抛出的error是VendingMachineError枚举成员中的任何一个,也就是说抛出的error并没有被定义在error枚举中,所以抛出为被定义的error信息将会有nourish(with:)
函数管理并且输出这个信息,如果说调用该函数抛出的不是VendingMachineError那么这个调用会被返回去,传播到正常的catch子句,因为在下面这个例子里面除了调用函数外就是另外一个Do-catch语句 这个Do语句还是会调用这个函数的抛出的error有不在定义里面,所以抛出的error非VendingMachineError就回执行一班catch子句在和最后的catch子句相结合组成新的error信息。
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)")
}
// 输出:Invalid selection, out of stock, or not enough money.
使用try?
来吧这个错误装换乘一个可选值,如果说在执行这个try?表达式的时候,error被抛出了那么这个时候该表达式的值就是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无论在什么时候都是函数返回的整数。因为函数返回的是整数,所以x和y将会是可选的整数值。
使用try?这样会使我们写出更加简洁的错误管理代码,当我们使用相同的方法来管理这些错误的时候。下面这几段代码是用几种方式来获取数据,如果说所有获取数据的方式都失败了那么该函数就回返回nil。
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
有些时候我们明知道某个throw函数或throw方法不会抛出错误,事实上它去在运行的时候抛错某个错误error。 在这种情况下我们要使用try!
在其表达式中禁用错误的传播。这会把调用包装在一个不会有错误抛出的运行时断言中。如果真的抛出了错误,你会得到一个运行时错误。
举个例子,下面的这段代码使用的是一个loadImage(atPath:)
函数,该函数会在一个给定的路径中加载图片资源,如果该资源没有被加载成功,那么该函数会抛出一个错误。其实这个图片是和应用程序绑定在一起的,运行时该错误不必抛出,所以我们用try!
来禁止该错误信息的传播。
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
使用defer
语句在即将离开当前代码块时执行一系列语句,该defer语句允许我们做一些清理工作,无论我们是如何离开当前代码块的,还是该清理工作是有抛出的error作出的,还是该代码块的清理工作是有return
或break
引起的。举个例子我们可以使用这个defer语句来却确保这个已关闭的文件描述器,或者手动释放内存。
一个defer语句将代码的执行延迟到当前的作用域退出之前,也就是说含有defer的语句要被延迟执行。延迟执行的语句不能含有控制转移语句。比如break和return语句,或者是抛出某个错误,这些都不能在defer语句里面出现。
在我们写的代码里延迟的这个动作会以一个相反的顺序进行执行的,比如说 第一个defer语句会被最后执行,第二个defer语句会被最后第二个执行,以此类推,最后一个defer语句会被首先执行。
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
// 最后执行这个
defer {
close(file)
}
// 先执行这个
while let line = try file.readline() {
}
}
}