Swift小知识点之错误处理常规处理方式 (二)
一、开发过程中常见的错误
常见的错误:
- 语法错误(编译报错)
- 逻辑错误(偏离预期需求)
- 运行时错误(可能会导致闪退,一般也叫做异常)
示例代码一:
func divide (_ num1:Int, _ num2:Int) -> Int { num1 / num2 //单一表达式的时候,我们可以省略return。 } print(divide(20, 0))
上面示例代码正常运行,但是如果把num2
传值为0
,系统就会报错(除数不能为0):

这种情况有两种解决办法:
- 返回类型修改为可选类型,如果被除数
num2
为0
,就返回nil
。
func divide (_ num1:Int, _ num2:Int) -> Int? { if num2 == 0 { return nil } return num1 / num2 //单一表达式的时候,我们可以省略return。 } print(divide(20, 0))
- 自定义错误信息,当传入
num2
的值为0
时报错(相比1,这样可以详细地告知开发者哪里出了错误)。
自定义错误信息步骤:
-
Swift中可以通过
Error
协议自定义运行时的错误信息(Error
是协议,也就是说枚举、结构体、类都可以定义错误信息)(造“雷”)。 -
函数内部通过
throw
抛出自定义自定义Error
,可能会抛出Error
的函数必须加上throws
声明 (埋“雷”) -
需要使用
try
调用可能会抛出Error
的函数(踩“雷”) - 处理Error(排“雷”)
示例代码二:
-
上面代码抛出的异常提示信息时我们自定义的,但是依然报错了,是因为我们只是让编译器能够抛出异常,我们并没有处理异常,程序运行过程中依旧会崩溃。要想程序正常运行,就需要捕获并处理异常。
二、处理Error
处理Error
有两种方式:
- 通过
do-catch
捕捉Error
- 不捕捉
Error
,在当前函数增加throws
声明,Error
将自动抛给上层函数
2.1. do-catch
示例代码:
///1. 造“雷” enum SomeError : Error { case illegalArg(String) //参数错误 case outOfBounds(Int,Int) //越界 case outOfMemory //内存 } ///2. 埋“雷” func divide (_ num1:Int, _ num2:Int) throws -> Int? { if num2 == 0 { throw SomeError.illegalArg("0不能作为除数") } return num1 / num2 //单一表达式的时候,我们可以省略return。 } ///4. 排“雷” func test() { print("1") do { ///3. 踩“雷” try divide(20, 0) } catch let SomeError.illegalArg(msg) { print("参数异常:",msg) } catch let SomeError.outOfBounds(size, index) { print("下标越界:","size = \(size)","index = \(index)") } catch let SomeError.outOfMemory { print("内存溢出") } catch { print("其它错误") } print("2") } test() ///打印: /* 1 参数异常: 0不能作为除数 4 Program ended with exit code: 0 */
把可能抛出异常的代码放到do
函数体内,后面的catch
类似于switch
的case
。
如果抛出异常后,调用try
异常函数的作用域内后面的代码都不会执行。直接进入catch
的匹配流程。
注意:
catch
如果没有自定义变量捕获error
,那么默认会有一个error
变量,在函数体内可以直接使用。
2.2. throws
如果最顶层函数(main
函数)依然没有捕捉Error
,那么程序将终止。
-
示例代码一(程序崩溃):
///1. 造“雷” enum SomeError : Error { case illegalArg(String) //参数错误 case outOfBounds(Int,Int) //越界 case outOfMemory //内存 } ///2. 埋“雷” func divide (_ num1:Int, _ num2:Int) throws -> Int? { if num2 == 0 { throw SomeError.illegalArg("0不能作为除数") } return num1 / num2 //单一表达式的时候,我们可以省略return。 } ///4. 排“雷” func test() throws { print("1") print(try divide(20,0)) ///3.踩“雷” print("2") } try test() ///打印: /* 输出: 1 Swift/ErrorType.swift:200: Fatal error: Error raised at top level: Swift_函数式编程.SomeError.illegalArg("0不能作为除数") */
-
示例代码二(程序正常):
func test() throws { print("1") do { print("2") print(try divide(200, 0)) print("3") } catch let error as SomeError { print(error) } print("4") } try test() /* 输出: 1 2 illegalArg("0不能作为除数") 4 */
-
示例代码三(catch不够详细):
catch
一定要能够处理所有情况,所以系统报错提示。
如下正常:func test() { do { print(try divide(200, 0)) } catch is SomeError { print("This is error") } catch { print("Other") } } test() // 输出:This is error
2.3. try?、try!
可以使用try?、try!
调用可能会抛出Error
的函数,这样就不用去处理Error
。
-
示例代码一:
func test() { var result1 = try? divide(200, 10) // Optional(20), 返回类型是Int? var result2 = try? divide(200, 0) // nil var result3 = try! divide(200, 10) // 2, 返回值类型是Int } test()
try?
返回可选类型,try!
会隐式解包。
-
示例代码二: 下面代码a和b是等价的:
var a = try? divide(200, 0) var b: Int? do { b = try divide(200, 0) } catch { b = nil // 可以忽略不写 }
2.4. rethrows
rethrows
声明:函数本身不会抛出错误,但调用闭包参数抛出错误,那么它会将错误向上抛。
-
示例代码:
func exec(_ fn:(Int, Int) throws -> Int, _ num1: Int, _ num2: Int) rethrows { print(try fn(num1, num2)) } try exec(divide, 20, 0) // 输出:Fatal error: Error raised at top level: SwiftTestDemo.SomeError.illegalArg("0不能作为除数"):
throws
表示函数内部可能会抛出异常,rethrows
表示函数参数可能会抛出异常。
空合并运算符??
的源码用到了rethrows
:
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
区别:
rethrows
换成throws
也是可以的,rethrows
的描述更加精确而已,开发者看到该标记会立即想到是函数参数(函数/闭包表达式)调用可能会抛出异常。
2.5. defer
defer
语句:用来定义以任何形式(抛错误、return
等)离开代码块前必须要执行的代码。
- 特点:
defer
语句将延迟至当前作用域结束之前执行。
通过文件管理案例看下如何使用:
// 打开文件(文件名) func open(_ filename: String) -> (Int, Data) { return (0, Data(count: 1024)) } // 关闭文件(文件id) func close(_ fileId: Int) { print("close") } // 处理文件 func processFile(_ filename: String) throws { let (fileId, file) = open(filename) // 使用file // ... try divide(20, 0) close(fileId) } try processFile("test.txt")
上面的示例正常情况下没有问题,如果抛出异常就无法执行close
,代码有可能会OOM
,这时候就需要在抛出异常前执行close
。总之在退出processFile
函数之前必须执行close
。
-
使用
defer
:func processFile(_ filename: String) throws { let (fileId, file) = open(filename) defer { close(fileId) } // 使用file // ... try divide(20, 0) } /* 输出: open close */
-
注意:所有代码一定不要写到异常代码之后,否则抛出异常后不会执行。
defer
语句的执行顺序与定义顺序相反:
func fn1() { print("fn1") } func fn2() { print("fn2") } func test() { defer { fn1() } defer { fn2() } } test() /* 输出: fn2 fn1 */
三、assert(断言)
很多编程语言都有断言机制:不符合指定条件就抛出运行时错误,常用于调试(Debug)阶段的条件判断。
默认情况下,Swift的断言只会在Debug模式下生效,Release模式下会忽略。
-
示例代码:
func divide(_ num1: Int, _ num2: Int) -> Int { assert(num2 != 0, "0不能作为除数") return num1 / num2 } print(divide(20, 0)) // Assertion failed: 0不能作为除数: file SwiftTestDemo/main.swift, line 13
增加Swift Flags修改断言的默认行为:
-assert-config Release
:强制关闭断言-assert-config Debug
:强制开启断言
四、fatalError(致命错误)
如果遇到严重问题,希望结束程序运行时,可以直接使用fatalError
函数抛出错误(这是无法通过do-catch
捕捉的错误)。
使用了fatalError
函数,就不需要再写return
。
func test(_ num: Int) -> Int { if num >= 0 { return 1 } fatalError("num不能小于0") }
在某些不得不实现、但不希望别人调用的方法,可以考虑内部使用fatalError
函数。