这节课我们一起来认识一下 Swift 中的错误处理
在调用方法和写一个轮子的时候,总会有各种各样奇奇怪怪的错误,就是已经正常编译的软件,也会出现一些不可预期的错误。不过,这些错误当中,有一些是可以被识别和捕捉的——它们可预期。
可预期的错误
为什么我们说有一些错误是可以预料得到的呢?比如说读取一个文件的时候文件不存在、保存一个文档的时候目录不可写、下载文件的时候网络无连接、传送一个参数的时候范围超出控制……这些错误都是可以预料得到的!我们完全没有必要让程序在这些错误里崩溃!
那么,在这些情况下我们就需要一个完善的异常处理机制,把这些可以预料到的异常都给捕捉起来,用友好的方式处理掉,避免我们的程序栽在这些已知的错误上。
抛出错误和抓住错误
我们调用了一个方法,如果必要,那你必须要明白这个方法是有风险的,它有可能会执行失败——这个时候它就会将失败的原因抛出来——抛出错误。
你必须将它可能抛出错误的情况都考虑好,如果这样报错,怎么办,如果那样报错,又怎么办!
所以,如果我们创建的方法可能有风险,就一定要用 throws 标记出来,这样调用的时候我们就可以用 catch 来接住这个炸弹。
创建一个包含错误状态的枚举
我们用枚举类型来创建可能的错误状况,只需要让枚举遵循 ErrorType 协议即可,其他的都交给编译器完成。
1 2 3 4 5 |
enum FireError:ErrorType { case overHeat(hot:Double) case outOfAmmo } |
比如说我们这里写两个错误,一个是枪管过热,另一个是没有子弹了!
有风险的方法
然后我们再来改造 Gun 的 fire() 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { guard ammo > 0 else { throw FireError.outOfAmmo } guard heat < 100 else { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
一个简单的模型,每次发射之前检查弹药数量,检查枪管温度,如果都 OK,就射击,同时枪管温度增加,弹药量减少。(这里温度设置不一定合常理哈)
处理错误
然后我们来调用这个方法11次看看会有什么样的情况发生:
1 2 3 4 5 6 7 8 9 10 11 |
var a = Gun() for _ in 0...10 { do { try a.fire() } catch FireError.overHeat(let heat) { print("枪管过热 \(heat) 度!") } catch FireError.outOfAmmo { print("Need reload!") } } |
最终,我们的到的结果是:
1 2 3 4 5 6 7 8 9 10 11 |
bang! bang! bang! bang! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! 枪管过热 112.0 度! |
Guard 是 if 吗?
答:不是。
如你所见,Guard 语法用起来好像很像是 if,但它不是,你可以理解为它是个反的 if , 如果我们反过来这样写也是完全可以的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if ammo > 0 { if heat < 100 { print("bang!") ammo-- heat += 20.5 } else { throw FireError.overHeat(hot: heat) } } else { throw FireError.outOfAmmo } } |
看起来是不是很绕?一堆一堆的大括号看着就眼晕!
使用 Guard 则就好像是个守门员——我们叫他“守门模式”,其实看起来就好像是反过来用 if:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if ammo < 0 { throw FireError.outOfAmmo } if heat > 100 { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
这样虽然一样了,你注意到没有?我们把大于小于号给翻转了!因为这样才能让 if 起到我们想要的作用,或者,我们可以这样用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Gun { var heat = 30.0 var ammo = 8 func fire() throws { if !(ammo > 0) { throw FireError.outOfAmmo } if !(heat < 100) { throw FireError.overHeat(hot: heat) } print("bang!") ammo-- heat += 20.5 } } |
这样大小的逻辑是摆正了,可是多出来的叹号更让人头大了,一不小心还容易扰不过这个弯儿……
所以,我们用 Guard,它也判断是非,如果“是”则不作用,如果“非”,就执行 else 里的内容,这样行内的逻辑对人来说就很直观了——不再需要曲线救国。
有风险的方法要放在 do – catch 块里
就像我前面的例子,我们把 fire 方法改造成能够抛出异常的方法,那么调用它的时候就必须把它放进特制的容器当中,这样编译器才能搞定和理解那些错误。
1 2 3 4 5 |
do { try someFunction that throws } catch error { } catch anotherError { } |
如上,我们在 do 的代码块当中来尝试方法(try),如果正常那皆大欢喜。如果方法抛出了异常,那我们就会执行到 catch 上——这里其实看起来好像 if else 的嵌套,也好像 switch 的选择——不过,catch 确实要求把可能会抛出的错误都写出来如果你不这么做,那么这个炸弹落地你的程序也就会被炸掉啦!
不过,就像 switch 一样,你也可以只写 catch 而不跟错误类型,这样就可以默认匹配所有错误了——缺点是你也不能知道发生了什么错误。
与调用者沟通
那我们做这个复杂的动作究竟有什么意义呢?直接把这些写成一个状态不更好?
还真不会更好。因为我们的方法并不一定就是给自己用,一个程序也不会只有一个人完成,当别人调用你的方法的时候,难道非得让人家先读一下800字的文档?在需要的位置抛出一个错误,写清楚错误的类型——调用者就可以方便地处理掉这个问题。
传球!
我们说遇到风险就要 try,而 try 出来的错误要 catch!可是没有 catch 住的炸弹怎么办?如果你不想拆弹呢?其实也可以,如果你在你的方法里调用了一个有风险的方法,而你又不想在这个方法里拆弹,那就不要管它就好了——当然,要记得把你的方法也标记为 throws ,这样这个炸弹会继续往下传!
呃,但你得记住,最终得有个方法来 catch 和拆弹,不然,你的程序还是会被炸掉。
如果一个问题得不到解决,它不会自行消失。
对了,如果你不想让错误抛出,那可以强制 try,使用关键字 try! ,这样就不需要拆弹了……因为遇到炸弹不用扔,直接炸。
本文由 落格博客 原创撰写:落格博客 » 总会报错:异常处理
转载请保留出处和原文链接:https://www.logcg.com/archives/1137.html