错误处理Error Handling
是响应错误以及从错误中恢复的过程。Swift 提供了在运行时对可恢复错误抛出,捕获,传递和操作的高级支持。
小节包含下面的知识点:
某些操作并不能保证所有代码都可以执行并或者都会产生出有用的结果,例如从磁盘读取文件并做数据处理时,任务会有多种失败的可能,可能是指定路径下文件不存在、文件不具有读取权限、文件编码格式不兼容等。区分这些错误情形可以让程序解决并处理一部分错误,然后把它解决不了的报告给用户。
注意,Swift 中的错误处理涉及到错误的处理模式,者会用到 Cocoa
和 Objective-C
中的 NSError
。即在 Swift 中使用 Objective-C
的类型。
需要说明一点的是,这一小节的内容对项目代码质量是很有意义的,但是应该有不少初学者并不擅长使用,譬如我,,,在使用 Objective-C
开发时,错误处理我也仅仅用过断言而已,其他的没用过甚至根本不了解。有时候,很多有错误预警的情况我们都会有各种的办法去标识,譬如弹窗、输出提醒等。在这里描述了几种不同的错误处理方式,至于哪种更好用,就需要自己以后在项目中去实践了。
表示并抛出错误
在 Swift 中,错误用遵循 ErrorType
协议类型的值来表示。这种空协议表示一种可以用作错误处理的类型。Swift 的枚举类型尤为适合塑造一组相关的错误情形 error conditions
,枚举的关联值 assiciated values
还可以提供额外的信息。比如可以这样表示:在游戏中操作自动贩卖机会出现的错误情形,抛出错误使用 throws
关键字:
// 定义错误类型,一个游戏自动贩卖机可能会出现的错误情形
enum VendingMachineError: ErrorType {
case InvalidSelection // 选择无效
case InsufficientFunds(coinsNeeded: Int) // 金币不足,并提示缺少数量
case OutOfStack // 缺货
}
// 抛出错误,抛出一个错误会对所发生的意外情况做出提示,表示正常的执行流程不能被执行下去。
throw VendingMachineError.InsufficientFunds(coinsNeeded: 10)
处理错误
某个地方错误被抛出时,那个地方的某部分代码必须要负责处理这个错误-比如纠正这个问题、尝试另外一种方式、或是给用户提示这个问题。Swift 中有 4 中处理错误的方式:一,把函数抛出的错误传递给调用此函数的代码;二,用 do-catch
语句处理错误;三,将错误作为可选类型处理了;四,断言此错误根本不会发生。
下面将会对这四种不同的错误处理方式进行说明。同时,为了快速定位到抛出错误的地方,在调用一个能抛出错误的函数、方法或者构造器前加上 try
关键字,或者 try?
、 try!
这样的变体。 Swift 中使用 try
、catch
、throw
这些方法的错误处理不涉及堆栈解退Stack unwinding
,性能很高。就此而言,throw
语句的性能特性是可以和 return
语句相当的。(P.S.这段话就是告诉我们不要担心处理错误会对程序造成什么负担,我们只要在需要抛出错误的地方抛出就OK了,Swift 语言本身已经做好了优化。)
用 throwing
函数传递错误
用 throws
关键字来标识一个可抛出错误的函数、方法或是构造器。在函数声明中的参数列表之后加上 throws
。一个标识了 throws
的函数被称为 throwing
函数。如果这个函数有返回值类型, throws
关键字需要写在箭头 ->
的前面:
// 不带返回参数的 throwing 函数
func canThrowErrors() throws {
// 函数体
}
// 带返回参数的
func canThrowErrors() throws -> String {
// 函数体
return "hello world"
}
一个 throwing
函数从内部抛出错误,并传递到该函数被调用所在的区域中。注意,只有 throwing
函数可以传递错误。任何在非 throwing
函数内部抛出的错误只能在此函数内部处理。
下面例子中 VendingMechine
类有一个 vend(itemNamed:)
方法,如果需要的物品不存在,缺货或者花费超过了已投入金额,该方法就会抛出一个对应的 VendingMachineError
。
// 定义错误类型,一个游戏自动贩卖机可能会出现的错误情形
enum VendingMachineError: ErrorType {
case InvalidSelection // 选择无效
case InsufficientFunds(coinsNeeded: Int) // 金币不足,并提示缺少数量
case OutOfStack // 缺货
}
// 商品属性
struct Item {
var price: Int // 价格
var count: Int // 数量
}
// 自动售卖机
class VendingMachine {
// 商品清单,注意,这里 price 这个属性名字是不能代码补全的,后面的 count 可以补全
var inventory = ["Candy Bar": Item(price: 12, count: 5),
"Chips": Item(price: 10, count: 4),
"Pretzel": Item(price: 15, count: 8)]
// 剩余金币数
var coinDeposited = 0
// 分配小吃方法,,,P.S. 还能学点英语单词呢
private func dispenceSnack(snack: String) {
print("dispence \(snack)") // 打印分发了什么小吃
}
// 抛出错误,至于 guard 的用法,前面有比较详细的说过
func vend(itemNamed name: String) throws {
guard var item = inventory[name] else {
throw VendingMachineError.InvalidSelection // 不存在选择的物品
}
guard item.count > 0 else {
throw VendingMachineError.OutOfStack // 囤货不足
}
guard item.price <= coinDeposited else {
throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinDeposited) // 金币不足
}
// 消费完毕后,自动售卖机存货发生改变
coinDeposited -= item.price
item.count -= 1
inventory[name] = item
dispenceSnack(name)
}
}
关于 guard
的用法前面有过比较详细的说明,点击查看 。这里在简单说一下吧:guard
用法同 if
,但是必须跟随一个 else
, 表示只要 guard
后面的语句成立,就一直往下走,否则走 else
语句。重点: guard
的作用范围是包含它的大括号!
throwing
函数只能传递错误,但是却不能处理错误–要么使用 do-catch
语句, try?
,或try!
来处理错误,要么继续将这些错误传递下去。
这里需要强调一点了, try
只能传递错误,并不能解决错误,而 try?
和try!
却可以。并且只要是调用可抛出错误的方法,就必须使用 try
这个关键字,至于后面加 ?
还是 !
,又或者什么都不加,就看个人需求了。
// 定义一个字典,表示每个人最喜欢的零食是什么
let favoriteSnacks = ["Jack": "Chips",
"Tom": "Milk",
"Rose": "Meat"]
// 购买喜欢额零食
func buyFavoriteSnacks(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
// 下面这个 try 必须添加,因为 调用一个 throwing 函数必须使用 try 来获取抛出的错误
// 这个其实仅仅是传递错误
try vendingMachine.vend(itemNamed: snackName)
}
用 Do-Catch
处理错误
可以使用一个 do-catch
语句运行一段闭包代码来做错误处理。如果在 do
语句中的代码跑出了一个错误,则这个错误将会与 catch
语句做匹配决定哪条语句能处理它(有点类似 Switch
的用法)。下面是 do-catch
语句的通用形式:
do {
try expression // throwing 函数
statements // 函数体,也是正常情况下的执行代码
} catch pattern 1 { // 匹配错误
statements
} catch pattern 2 where condition {
// 这里的 where condition 我并不知道有什么具体的作用,教材中是这么写的,但是下面的实例代码中却没有体现这一点。暂时算是一个迷惑点吧,但是并不会影响到使用。
statements
}
在 catch
后面写一个模式 pattern
来表示这个语句能处理什么样的错误。如果一个 catch
语句没有带一个模式,那么这条语句可以和任何错误匹配(类似 Switch
中的 default
),并且把错误和一个名字为 name
的局部常量做绑定。(这句话我暂时并没有理解,这个 name
是什么也没搞明白,教材中提供了一个 Patterns
的参考链接,点击查看)
catch
语句不必将 do
语句中代码所抛出的每个可能的错误都处理。如果没有一条 catch
语句来处理错误,错误就会传播到周围的作用域。然而错误还是必须要被某个周围的作用域处理的:要么是一个外围的 do-catch
错误处理语句,要么是一个 throwing
函数的内部。
举例来说,下面的代码处理了 VendingMachineError
枚举类型的全部三个枚举实例,但是所有其他的错误,即不属于以上三种情况的,就必须由它周围作用域去处理。
// 实例自动售卖机
var vendingMachine = VendingMachine()
vendingMachine.coinDeposited = 8
do {
// 这里就承接了上面代码中传递过来的错误
try buyFavoriteSnacks("Tom", vendingMachine: vendingMachine)
// 这里执行正常情况下,即没有错误抛出时的代码
print("执行没有错误抛出时的代码")
} catch VendingMachineError.InvalidSelection {
print("选择物品没有时要怎么办")
} catch VendingMachineError.OutOfStack {
print("缺货了怎么办")
} catch VendingMachineError.InsufficientFunds(let coinNeeded) { // 括号内部是传递过来的值
print("金币不足,缺少 \(coinNeeded)")
}
// 输出:金币不足,缺少 2
// 如果:vendingMachine.coinDeposited = 10,输出:没有错误时的代码
// 如果:try buyFavoriteSnacks("Tom", vendingMachine: vendingMachine), 输出:选择物品没有时要怎么办
// 注意注意: 上面代码中 VendingMachineError 后面的点内容是不能代码补全的
其实对于上面所说的 “但是所有其他的错误,即不属于以上三种情况的,就必须由它周围作用域去处理”,怎么去处理,我暂时并不是很清楚,个人感觉是在最后写一个 catch
,不加任何条件,类似 switch
中的 default
,但是自己并不确定是否要真么处理,也就没有写。
将错误转换成可选值
可以使用 try?
通过将其转化成一个可选值来处理错误。如果在评估 try?
表达式时一个错误被抛出,那么这个表达式的值就为 nil
。
个人理解,这里类似一刀切了,并没有上面示例代码中利用枚举值判断抛出的不同的错误。使用 try?
的时候,只要抛出错误,就直接令表达式为 nil
,并不会在意到底出了什么错误。在大多数情况下,使用 try?
可以写出简洁的错误处理代码。
下面这个代码片段展示 try?
的基本用法以及和 do-catch
的对比,其中 x
、 y
处理的效果是一样的:
func someThrowErrorsFuncation() throws -> Int {
// 函数体,包含抛出的错误代码
}
// 利用 try? 处理错误
let x = try? someThrowErrorsFuncation()
// 和 try? 效果一样的 do-catch 处理语法
let y: Int?
do {
try someThrowErrorsFuncation()
} catch {
y = nil
}
注意,使用 try?
时,无论表达式返回的是什么类型,那么作为接收的表达式都是可选类型,就像上面代码中的 x
和 y
。
如果方法失败都返回 nil
,即抛出的错误时使用 nil
,那么就用 try?
吧,很简洁:
// 获取数据的方法
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data } // 如果能成功从磁盘获取数据
if let data = try? fetchDataFromServer() { return data } // 如果能成功从服务器获取数据
return nil // 无法获取数据
}
使错误传递失效
有时候我们写代码的时候知道某个 throwing
函数实际上在运行时是不会抛出错误的。在这种条件下,可以再表达式前写 try!
来使错误传递失效,并把调用包装在一个运行时断言 runtime assertion
中来断定不会有错误抛出。如果实际上却是抛出了错误,那么就会得到一个运行时的错误。
下面是一个加载图片的函数 loadImage(_:)
,函数在指定路径下加载图片,如果图片不能被载入则抛出一个错误。示例代码中加载和应用程程绑定的图片,即 APP 内部的图片,运行时显然不会有错误抛出的,此时可以使用错误传递失效:
let photo = try! loadImage("./Resources/default.png")
指定清理错误
可以使用 defer
语句在代码执行到离开当前代码段之前去执行一套语句,改语句让我们能够做一些应该被执行的必要清理工作,不管是以何种方式离开当前代码段的 – 无论是由于抛出错误而离开,或是因为一条 return
或者 break
、 continue
等这样的类似语句。
defer
语句将代码的执行延迟到当前的作用域退出之前。该语句由 defer
关键字和要被延时执行的语句组成。延迟执行的语句不会包含任何会将控制权移交到语句外面的代码,例如 break
或是 return
语句,或是抛出一个错误。
延时执行的操作是按照他们指定的相反顺序执行 – 意思是第一条 defer
语句中的代码执行实在第二条 defer
语句中代码被执行之后,以此类推。即使没有涉及到错误处理的代码,也可以使用 defer
语句。
下面示例代码用了一条 defer
语句来确保 open(_:)
函数有一个相对应的 close(_:)
调用:
// 文件
struct HandleFile {
func readline() throws -> String {
// 读取文件
return "hello world"
}
}
// 判断文件是否存在的方法
func exists(file: String) -> Bool {
// 执行代码,这里假设返回 true
return true
}
// 打开文件
func open(file: String) -> HandleFile {
let file = HandleFile()
return file
}
// 关闭文件
func close(file: HandleFile) {
}
// 这个方法才是展示 defer 用法的地方,上面的一堆代码只为保证下面这个方法执行的时候不会报错
func processFile(fileName: String) throws {
if exists(fileName) {
let file = open(fileName)
defer {
close(file)
}
while let line = try? file.readline() {
// 处理文件
print(line)
}
}
}
关于错误处理的用法即使写完了我也是一头雾水,因为自己以前几乎没用过,而新学的知识又很难想到用在哪里,譬如错误传递失效,既然明知道不会抛出错误,又为什么要写 throwing
方法,什么时候实际开发中需要用到这个,还有 defer
,具体什么时候用,有哪些应用场景,自己以前都是没有遇到的,需要以后慢慢去摸索。同时,自己不理解的时候也查了下其他的文章怎么写的,汉语的找到一篇不错的,点击查看,写者明显更有经验些,可以多作参考。
昨天晚上看了小李子的奥斯卡作品《荒野猎人》,真的是一路高能啊,隔着屏幕都能感觉到疼,感觉到冷。