15-错误处理

210 阅读5分钟

Swift 错误处理

开发过程中的常见错误

// 1. 语法错误(编译时报错)
// let x = 10 +  // 语法错误,会在编译时发现

// 2. 逻辑错误(程序运行但结果不正确)
func add(a: Int, b: Int) -> Int {
    return a * b  // 逻辑错误:应该是加法但写成了乘法
}

// 3. 运行时错误(可能导致程序崩溃)
// let array = [1, 2, 3]
// let item = array[10]  // 运行时错误:数组越界

自定义错误

在swift中可以通过Error协议自定义运行时的错误信息

enum SomeError: Error {
    case illegalArg(String)
    case outOfBounds(Int, Int)
    case outOfMemory
}

函数内部通过throw抛出自定义Error,可能会抛出Error的函数必须加上throws声明

func divide(_ a: Int, _ b: Int) throws -> Int {
    guard b != 0 else { throw SomeError.illegalArg("0 不能做除数") }
    return a / b
}

需要使用try调用可能会抛出Error的函数

var result = try divide(20, 10)

do catch

先看下面的例子

func test() {
    print("1")
    do {
        print("2")
        print(try divide(20, 0))
        print("3")
    } catch let SomeError.illegalArg(msg) {
        print("参数异常:", msg)
    } catch let SomeError.outOfBounds(size, index) {
        print("下标越界:", "size=\(size)", "index=\(index)")
    } catch SomeError.outOfMemory {
        print("内存溢出")
    } catch {
        print("其他错误")
    }
    print("4")
}

这里的代码需要注意点是 catch let SomeError.illegalArg(msg)这些语句,其实这里后面几个catch,这里类似于swift中case的情况,当前面的error和当前错误类型匹配的时候,那么就会产生illegalArg这样一个枚举类型,当然了要产生这样一个枚举类型,必须要把msg传进来。还有我们可以看到case 后面的let,代表的是msg是let类型。而let SomeError.outOfBounds(size, index)代表的是size,index都是let类型。当然如果你不想写那么多catch,可以使用下面的写法,下面的写法本质其实是用switch 代替了一部分catch。

do {
    try divide(20, 0)
} catch let error {
    switch error {
    case let SomeError.illegalArg(msg):
        print("参数错误: ", msg)
    default:
        print("其他错误")
    }
}

抛出Error后,try下一句直到作用域结束的代码都将停止运行,这个特性其实和别语言的try,catch一样的

处理Error

处理 Error 的2种方式

  1. 通过 do-catch 捕捉 Error2) 不捕捉 Error,在当前函数增加 throws 声明,Error 将自动抛给上层函数
  2. 如果最顶层函数(main 函数)依然没有捕捉 Error,那么程序将终止.
  3. 其实也可以使用可以使用try?、try!调用可能会抛出Error的函数,这样就不用去处理Error

其实对于错误我们除了在当前调用的地方进行处理,也有可能我们当前调用的地方不想处理,那么我会继续往上抛。

func test() throws {
    print("1")
    print(try divide(20, 0))
    print("2")
}

try test()
// 1
// Fatal error: Error raised at top level
do {
    print(try divide(20, 0))
} catch is SomeError {
    print("SomeError")
}

这里其实很好理解,主要有一个地方,is这里是怎么理解的?,看下面的swift中as.

func test() throws {
    print("1")
    do {
        print("2")
        print(try divide(20, 0))
        print("3")
    } catch let error as SomeError {
        print(error)
    }
    print("4")
}

try test()
// 1
// 2
// illegalArg("0不能作为除数")
// 4

swift 中 is

is用于“类型检查”,判断一个值在运行时是否为某个类型或是否遵循某个协议,返回 Bool,其实就是判断是不是某个类型或者是不是遵守某个协议。注意is是只检查类型,如果你想转换类型那应该使用as相关的

// 1) 检查类/结构体/枚举实例类型
let v: Any = "hello"
if v is String { /* true */ }
if v is Int { /* false */ }

// 2) 检查协议一致性
protocol P {}
struct S: P {}
let s: Any = S()
if s is P { /* true */ }

// 3) 与 as?/as! 配合(先 is 再转换)
if v is String, let str = v as? String { print(str.count) }

// 4) switch 中的类型分发
switch v {
case is Int: print("int")
case is String: print("string")
default: break
}

// 5) catch 中匹配错误类型
do {
    try divide(20, 0)
} catch is SomeError {
    print("命中 SomeError 的任意 case")
}

// 6) 与集合一起用
let arr: [Any] = [1, "a", 2.0]
let ints = arr.compactMap { $0 as? Int }    // 或 arr.filter { $0 is Int }

使用try?try! 处理异常

可以使用 try?、try! 调用可能会抛出 Error 的函数,这样就不用去处理 Error

func test() {
    print("1")

    var result1 = try? divide(20, 10) // Optional(2), Int?
    var result2 = try? divide(20, 0)  // nil
    var result3 = try! divide(20, 10) // 2, Int

    print("2")
}
test()

下面两种写法的 a和b其实是等价的

var a = try? divide(20, 0)

var b: Int?
do {
    b = try divide(20, 0)
} catch {
    b = nil
}

rethrows

rethrows 表明:函数本身不会抛出错误,但调用闭包参数抛出错误,那么它会将错误向上抛

func exec(_ fn: (Int, Int) throws -> Int, _ num1: Int, _ num2: Int) rethrows {
    print(try fn(num1, num2))
}

// Fatal error: Error raised at top level
try exec(divide, 20, 0)

defer 

其实类似于别的语言中错误处理的finnly

  1. defer 语句:用来定义以任何方式(抛错误、return 等)离开代码块前必须执行的代码
  2. defer 语句将延迟至当前作用域结束之前执行
  3. defer 语句的执行顺序与定义顺序相反
func open(_ filename: String) -> Int {
    print("open")
    return 0
}

func close(_ file: Int) {
    print("close")
}
func processFile(_ filename: String) throws {
    let file = open(filename)
    defer {
        close(file)
    }

    // 使用 file
    // ...
    try divide(20, 0)   // close 将会在这里调用(作用域结束前)
}

try processFile("test.txt")
// open
// close
// Fatal error: Error raised at top level

defer 语句的执行顺序与定义顺序相反

func fn1() { print("fn1") }
func fn2() { print("fn2") }

func test() {
    defer { fn1() }
    defer { fn2() }
}

test()
// fn2
// fn1

assert

  1. 很多编程语言都有断言机制:不符合指定条件就抛出运行时错误,常用于调试(Debug)阶段的条件判断
  2. 默认情况下,Swift 的断言只会在 Debug 模式下生效,Release 模式下会忽略
func divide(_ v1Int_ v2Int) -> Int {
    assert(v2 != 0"除数不能为0")
    return v1 / v2
 }

print(divide(200))

image.png

fatalError

如果遇到严重问题,希望结束程序运行时,可以直接使用 fatalError 函数抛出错误(这是无法通过 do-catch 捕捉的错误)。简单来说我现在错误不想让你捕捉了,就是要闪退,就可以用这个。

func test(_ num: Int) -> Int {
    if num >= 0 {
        return 1
    }
    fatalError("num不能小于0")
}

在某些不得不实现、但不希望别人调用的方法,可以考虑内部使用 fatalError 函数

class Person { required init() {} }

class Student: Person {
    required init() { fatalError("don't call Student.init") }
    init(score: Int) {}
}

var stu1 = Student(score: 98)
var stu2 = Student()