错误处理是 Swift 2.0 推出的新特性,如果想知道关于更多的 Swift 2.0 新特性,请参考我的另一篇文章:Swift 2.0 新特性。
在原来的 Cocoa 开发中,我们通常使用 NSError
对象来标识一个错误。我们可以为一个可能发生错误的方法传一个 NSError
对象的指针(在 Swift 当中是 inout 类型的参数),当这个方法发生错误时,它会对 NSError
对象进行赋值,我们便能获取错误的具体信息。这种方法的弊端在于,我们可以通过传入一个 nil
来忽略错误,或者在拿到错误对象后压根不对它进行处理。
Swift 2.0 加强了错误处理的安全性,我们可以使用 throws
关键字来表示一个方法可能抛出错误,这与 C++
或 Java
中的异常处理很相似。通过抛出错误,我们可以将控制权转交给方法调用者。
当接收到调用方法抛出的错误时,调用者有两种选择:
- 将错误继续向上抛出,这意味着调用者不对错误进行处理。
- 接收并处理错误,这要使用到 Swift 2.0 的新关键字
catch
。在接收到错误的同时,可以得到关联的错误对象,并获取错误的详细信息。
在 Swift 当中,错误对象必须遵守 ErrorType
协议。Cocoa 类库中使用 NSError
实例作为错误对象,而在自己的代码中,我们可以自定义错误类型。要自定义错误类型,只需要新建一个遵守了 ErrorType
协议的枚举类型,枚举中的每个值都代表不同的错误状态。由于 Swift 中的枚举能够使用关联值,这意味着我们能够为不同的错误状态提供关联的详细信息。
叨叨地讲了这么多理论知识,估计你也看烦了,说得再多,不如上代码来得直观。
先新建一个 playground
,并输入下面的代码:
import UIKit
// 1
class BookShelf: CustomStringConvertible {
// 有一个好大的书架
private let maximumBooks = 1000
private(set) var books: Int = 0
var description: String {
return "书柜当前还有\(books)本书"
}
// 2
func purchaseBooks(count: Int) {
books += count
if books > maximumBooks {
fatalError("你的书太多了,书柜已经放不下了")
}
}
// 3
func lendBooks(count: Int) {
books -= count
if books < 0 {
fatalError("你都没书了,还要借给谁?")
}
}
}
// 4
func starterShelf() -> BookShelf {
let myShelf = BookShelf()
myShelf.purchaseBooks(100)
return myShelf
}
这段代码很简单,而且还没有用到错误处理:
- 定义一个书架的类型,并遵守了
CustomStringConvertible
协议,用来方便地打印出这个类型的信息。 - 定义一个买书的方法,并判断书架是否还能放下刚买的新书。
- 定义一个借书的方法,并判断书架上是否还有剩余的书。
- 这个方法用来创建一个初始状态的书架。
接着,我们来将这段代码改造改造,好让它能使用错误处理。
首先,先定义一个错误处理类型,在 import
下面添加如下代码:
enum BookShelfError: ErrorType {
case NoEnoughSpace(required: Int)
case OutOfBooks
}
就像前面所说的,我们定义了一个枚举,并让它遵守了 ErrorType
协议。这里定义了两个错误状态,同时还为NoEnoughSpace
状态提供了一个关联值,用来指出还差几个空位。
接着,将买书与借书两个方法修改成如下:
func purchaseBooks(count: Int) throws {
if books + count > maximumBooks {
let required = (books + count) - maximumBooks
throw BookShelfError.NoEnoughSpace(required: required)
}
books += count
}
func lendBooks(count: Int) throws {
if count > books {
throw BookShelfError.OutOfBooks
}
books -= count
}
修改后,starterShelf
方法会报错,暂时先不管它。
在上面的代码中,我们使用了两个新的关键字 throws
和 throw
,前一个关键字用来表明该方法可能抛出错误,后一个则在发生错误用来抛出错误。
其它修改都是很直观的,需要注意的一点是当抛出 NoEnoughSpace
错误时,还附带了一个关联值,这个值用来表示书柜缺少的空位。
再来看看 starterShelf
中的错误,因为这个函数调用了 purchaseBooks
方法,所以我们需要捕获错误,或者再次抛出错误。在这里,我们先将它抛出:
func starterShelf() throws -> BookShelf {
let myShelf = BookShelf()
try myShelf.purchaseBooks(100)
return myShelf
}
对于一个可能抛出错误的方法,需要使用 try
关键字来调用它,由于要将错误再次抛出,所以同时使用了 throws
关键字来表明我们不准备处理这个错误。
但是,由于我们知道书架最多能放 1000 本书,这里的调用是绝对不会出现异常的。对于这种情况,我们可以用 forced-try 表达式,语法形式是在 try
后面加上感叹号,即 try!
,表明我们知道方法调用不会出现异常,可以对其进行强制调用,同时也就不再需要指定 throws
关键字:
func starterShelf() -> BookShelf {
let myShelf = BookShelf()
try! myShelf.purchaseBooks(100)
return myShelf
}
至此,所有的修改都已经完成了,接着就来试下这个类好不好使:
let shelf = starterShelf()
do {
try shelf.purchaseBooks(5000)
} catch BookShelfError.NoEnoughSpace(let required) {
print("书柜满啦,新买的书放不下了。还差\(required)个空位,赶紧买个新书柜吧")
}
shelf
在这段代码中,使用了 do-catch
语句来对错误进行捕获,在错误对象中还使用了关联值来取得所需要的书柜空位数。
执行以上代码,可以看到控制台打印出了异常信息,也就是调用 purchaseBooks
方法没有成功,此时,shelf
变量中的书本数应该还是 100。
再测试一下另一个方法:
do {
// 这行可以顺利执行
try shelf.lendBooks(50)
// 这行会抛出一个错误
try shelf.lendBooks(200)
} catch BookShelfError.OutOfBooks {
print("书都没啦,你还借给谁啊")
}
shelf
这里的代码与上面的代码基本一样,对方法进行调用,然后捕获错误。可以看到 shelf
中的书本数是 50,所以可以确实,异常是在方法第二次调用时才抛出的。
完整的 playground
代码可以在 我的github 下载到。