Swift小知识点之错误处理常规处理方式 (二)

一、开发过程中常见的错误

常见的错误:

  • 语法错误(编译报错)
  • 逻辑错误(偏离预期需求)
  • 运行时错误(可能会导致闪退,一般也叫做异常)

示例代码一:

func divide (_ num1:Int, _ num2:Int) -> Int {
    num1 / num2   //单一表达式的时候,我们可以省略return。
}
print(divide(20, 0))

上面示例代码正常运行,但是如果把num2传值为0,系统就会报错(除数不能为0): 

这种情况有两种解决办法:

  1. 返回类型修改为可选类型,如果被除数num20,就返回nil
    func divide (_ num1:Int, _ num2:Int) -> Int? {
        if num2 == 0 {
            return nil
        }
      return num1 / num2   //单一表达式的时候,我们可以省略return。
    }
    print(divide(20, 0))
  1. 自定义错误信息,当传入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类似于switchcase

如果抛出异常后,调用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函数。 

 

posted on 2022-03-17 22:32  梁飞宇  阅读(239)  评论(0)    收藏  举报