前言
在 Swift 1.0 时代是没有异常处理和抛出机制的,如果要处理异常,要么使用 if else 语句或 switch 语句判断处理,要么使用闭包形式的回调函数处理,再要么就使用 NSError 处理。以上这些方法都不能像 Java 中的 try catch 异常控制语句那样行如流水、从容不迫的处理异常,而且也会降低代码的可读性。当 Swift 2.0 到来后,一切都不一样了。在 Swift 2.0 中 Apple 提供了使用 throws、throw、try、do、catch 这五个关键字组成的异常控制处理机制。
-
1)如何建造异常类型:
在 iOS 开发当中,我们会面对很多异常处理。在 Cocoa Touch 中我们使用 NSError 来进行异常处理。在新的 Swift 2.0 中,我们可以使用新的 ErrorType protocol。
-
在 Swift 中,enum 是最好的方法建立属于你自己的异常类型,你只要在你的 enum 中确认新的 ErrorType。
enum MyError: ErrorType { case NotExist case OutOfRange }
-
2)如何抛出异常:
-
在抛出异常之前,我们需要在函数或方法的返回箭头 -> 前使用 throws 来标明将会抛出异常。
func myMethodRetrunString() throws -> String // No return, we can just add throws in the end func myMethodRetrunNothing() throws
-
声明之后,我们需要在函数或者方法里扔出异常,很简单使用 throw 就可以了。
func myMethod() throws { // ... // item is an optional value guard let item = item else { // need throws the error out throw MyError.NotExist } // do with item }
上面这段代码使用了 guard 来进行 unwrap optional value。这是 Swift 2.0 提供的一个新的方法。
-
-
3)如何获取并处理异常:
-
上文讲述了如何建造抛出异常,获取和处理异常就变得很简单了。使用 do-catch 机制。
do { try functionWillThrowError() } catch { // deal with error }
-
do-catch 机制简单易懂。很多编程语言也使用类似的机制进行异常处理,但是在 Swift 中有一个比较重要的特性。catch 和 switch 一样具有 Pattern Matching 的能力。所以,使用 catch 你可以对异常的解析进行更为高级的处理。
do { try functionWillThrowError() } catch MyError.NotExist { // deal with not exist } catch MyError.OutOfRange { // deal with not exist }
这里值得提一下在 Swift 2.0 中一个跟异常处理没有关系的改进。Swift 2.0 中没有了 do-while 循环,取而代之的是 repeat-while。苹果说这个改动是为了增强代码的可读性。但是我更觉得是为了让我们更舒服的使用 do-catch。
-
-
4)不处理异常:
-
如果我不想处理异常怎么办,或者说,我非常确定某个方法或者函数虽然声明会抛出异常,但是我自己知道我在使用时候是绝对不会抛出任何异常的。这种情况下 我们可以使用 try!
try! functionThrowErrorNil()
当然,如果你使用 try!,而你的方法或者函数抛出了异常,那么你会得到一个运行中异常 (runtime error) 所以我们开发者需要慎用哦。
-
-
5)总结:
- 使用 ErrorType 的帮助建立你的异常类型。
- 使用 throws 来声明异常,用 throw 来抛出异常。
使用 do-catch 机制来获取和处理异常。
-
下面我们来举例看看如何使用,我用使用手机刷朋友圈为例。首先我们需要定义异常枚举,在 Swift 2.0 中 Apple 提供了 ErrorType 协议需要我们自定义的异常枚举遵循:
enum WechatError: ErrorType { case NoBattery // 手机没电 case NoNetwork // 手机没网 case NoDataStream // 手机没有流量 }
-
我们定义了导致不能刷微信的错误枚举 WechatError。然后定义一个检查是否可以刷微信的方法 checkIsWechatOk():
func checkIsWechatOk(isPhoneHasBattery: Bool, isPhoneHasNetwork: Bool, dataStream: Int) throws { guard isPhoneHasBattery else { throw WechatError.NoBattery } guard isPhoneHasNetwork else { throw WechatError.NoNetwork } guard dataStream > 50 else { throw WechatError.NoDataStream } }
-
这里注意,在方法名后有 throws 关键字,意思为该方法产生的异常向上层抛出。在方法体内使用 guard 语句对各种状态进行判断,然后使用 throw 关键字抛出对应的异常。然后我们定义刷微信的方法:
func playWechat(isPhoneHasBattery: Bool, isPhoneHasNetwork: Bool, dataStream: Int) { do { try checkIsWechatOk(isPhoneHasBattery, isPhoneHasNetwork: isPhoneHasNetwork, dataStream: dataStream) print("放心刷,刷到天昏地暗!") } catch WechatError.NoBattery { print("手机都没电,刷个鬼啊!") } catch WechatError.NoNetwork { print("没有网络哎,洗洗玩单机吧!") } catch WechatError.NoDataStream { print("没有流量了,去蹭Wifi吧!") } catch { print("见鬼了!") } } playWechat(true, isPhoneHasNetwork: true, dataStream: 60) // 放心刷,刷到天昏地暗! playWechat(true, isPhoneHasNetwork: false, dataStream: 60) // 没有网络哎,洗洗玩单机吧! playWechat(false, isPhoneHasNetwork: true, dataStream: 60) // 手机都没电,刷个鬼啊! playWechat(true, isPhoneHasNetwork: true, dataStream: 30) // 没有流量了,去蹭Wifi吧!
上述的代码示例中,首先检查是否可以刷微信的方法前使用 try 关键字,表示允许该方法抛出异常,然后使用了 do catch 控制语句捕获抛出的异常,进而做相关的逻辑处理。
这套异常处理机制使 Swift 更加的全面和安全,并且提高了代码的可读性,非常棒。
1、Guard
在 Haskell, Erlang 等语言中早已存在 guard,在这里有更多关于它的介绍。guard 语句和 if 语句有点类似,都是根据其关键字之后的表达式的布尔值决定下一步执行什么。但与 if 语句不同的是,guard 语句只会有一个代码块,不像 if 语句可以 if else 多个代码块。guard 必须强制有 else 语句。guard 中的 else 只能执行转换语句,像 return, break, continue 或者 throws,当然你也可以在这里返回一个函数或者方法。那么 guard 语句的作用到底是什么呢?顾名思义,就是守护。guard 语句判断其后的表达式布尔值为 false 时,才会执行之后代码块里的代码,如果为 true,则跳过整个 guard 语句,我们举 例来看看。
-
我们以今年高考为例,在进入考场时一般都会检查身份证和准考证,我们写这样一个方法:
func checkup(person:[String:String!]) { // 检查身份证,如果身份证没带,则不能进入考场 guard let id = person["id"] else { print("没有身份证,不能进入考场!") return } // 检查准考证,如果准考证没带,则不能进入考场 guard let examNumber = person["examNumber"] else { print("没有准考证,不能进入考场!") return } // 身份证和准考证齐全,方可进入考场 print("您的身份证号为:\(id),准考证号为:\(examNumber)。请进入考场!") } checkup(["id": "123456"]) // 没有准考证,不能进入考场! checkup(["examNumber": "654321"]) // 没有身份证,不能进入考场! checkup(["id": "123456", "examNumber": "654321"]) // 您的身份证号为:123456,准考证号为:654321。请进入考场!
上述代码中的第一个 guard 语句用于检查身份证,如果检查到身份证没带,也就是表达式为 false 时,执行大括号里的代码,并返回。第二个 guard语 句则检查准考证。如果两证齐全,则执行最后一个打印语 句,上面的两个 guard 语句大括号内的代码都不会执行,因为他们表达式的布尔值都是 true。
这里值得注意的是,id 和 examNumber 可以在 guard 语句之外使用,也就是说当 guard 对其表达式进行验证后,id 和 examNumber 可在整个方法的作用域中使用,并且是解包后的。
-
我们再用 if else 语句写一个类似的方法:
func checkupUseIf(person: [String: String!]) { if let id = person["id"], let examNumber = person["examNumber"] { print("您的身份证号为:\(id),准考证号为:\(examNumber)。请进入考场!") } else { print("证件不齐全,不能进入考场!") } print("您的身份证号为:\(id),准考证号为:\(examNumber)") // 报异常 } checkupUseIf(["id": "123456"]) // 证件不齐全,不能进入考场! checkupUseIf(["examNumber": "654321"]) // 证件不齐全,不能进入考场! checkupUseIf(["id": "123456", "examNumber": "654321"]) // 您的身份证号为:123456,准考证号为:654321。请进入考场!
我们可以看到用 if else 实现的方法显然不如 guard 实现的那么精准。而且 id 和 examNumber 的作用域只限在 if 的第一个大括号内,超出这个作用域编译就会报错。
通过上述两个小例子不难看出,guard 语句正如一个称职的守卫,层层把关,严防一切不允许发生的事,并且让代码具有更高的可读性,非常棒。
2、Defer
-
在一些语言中,有 try/finally 这样的控制语句,比如 Java。这种语句可以让我们在 finally 代码块中执行必须要执行的代码,不管之前怎样的兴风作浪。在 Swift 2.0 中,Apple 提供了 defer 关键字,让我们可以实现同样的效果。
func checkSomething() { print("CheckPoint 1") doSomething() print("CheckPoint 4") } func doSomething() { print("CheckPoint 2") defer { print("Clean up here") } print("CheckPoint 3") } // CheckPoint 1, CheckPoint 2, CheckPoint 3, Clean up here, CheckPoint 4 checkSomething()
-
上述示例可以看到,在打印出 “CheckPoint 2” 之后并没有打印出 “Clean up here”,而是 “CheckPoint 3”,这就是 defer 的作用,它对进行了 print("Clean up here") 延迟。我们再来看一个 I/O 的示例:
// 伪代码 func writeSomething() { let file = OpenFile() let ioStatus = fetchIOStatus() guard ioStatus != "error" else { return } file.write() closeFile(file) }
-
上述示例是一个 I/O 操作的伪代码,如果获取到的 ioStatus 正常,那么该方法没有问题,如果 ioStatus 取到的是 error,那么会被 guard 语句抓到执行 return 操作,这样的话 closeFile(file) 就永远都不会执行了,一个严重的 Bug 就这样产生了。下面我们看看如何用 defer 来解决这个问题:
// 伪代码 func writeSomething() { let file = OpenFile() defer { closeFile(file) } let ioStatus = fetchIOStatus() guard ioStatus != "error" else { return } file.write() }
我们将 closeFile(file) 放在 defer 代码块里,这样即使 ioStatus 为 error,在执行 return 前会先执行 defer 里的代码,这样就保证了不管发生什么,最后都会将文件关闭。
-
在你的代码块就要结束前。如果你使用了 defer。在其之中的代码就会运行。等于说,给了你最后的机会来进行一些处理。如果你熟悉 BDD 或者 TDD,那么你可以参考他们中的 aferAll 机制。
func myFunction() throws { defer { // No matter what happened I need do something print("All done, clean up here") } guard let item = item else { // need throws the error out throw MyError.NotExist } guard item.count > maxNumber else { // need throws the error out throw MyError.OutOfRange } // do something with item // ... }
注意,如果你有多个 defer 语句,他们在执行的顺序会和栈一样,最后一个进,第一个出。