美文网首页
Swift进阶十:错误处理

Swift进阶十:错误处理

作者: Trigger_o | 来源:发表于2022-06-08 11:58 被阅读0次

通过将函数标记为 throws 来表示可能会出现失败的情况。除了调用者必须处理成功和失败的情况的语法以外,和可选值相比,能抛出异常的方法的主要区别在于,它可以给出一个包含所发生的错误的详细信息的值.
在网络请求中,有很多事情可能会发生错误,比如当前没有网络连接,或者无法解析服务器的返回等等。带有信息的错误在这种情况下就对调用者非常有用了,它们可以根据错误的不同来采取不同的对应方法,或者可以提示用户到底哪里发生了问题.

一:Result类型

Result 类型和可选值非常相似。
前面讲到可选值是枚举类型,有两个成员,一个不包含关联值的 .none 或者 nil,以及一个包含关联值的 some.
Result 类型也是两个成员组成的枚举:一个代表失败的情况,并关联了具体的错误值;另一个代表成功的情况,它也关联了一个值。和可选值类似,Result 也有一个泛型参数.

大概是这个样子,其中Error是协议

enum Result<A> { 
    case failure(Error)
    case success(A) 
}

举一个例子,一个读取文件的方法,它返回一个可选的结果,因为可能读取失败

func contentsOrNil(ofFile flename: String) -> String?

但是为什么会失败,原因没有返回,所以我们把错误的原因定义成枚举

enum FileError: Error { 
    case fileDoesNotExist  //文件不存在
    case fileIsEmpty(name:String) //文件是空的
    case noPermission  //没有读取权限
}

然后改造方法 ,现在它要么返回一个有效的结果,要么返回一个Error,Error上面已经定义了FileError

func contents(ofFile flename: String) -> Result<String>

现在就可以这么使用了,如果是.success,那么输出内容,如果是.failure,那么处理错误

let result = contents(ofFile: "input.txt")
switch result { 
    case let .success(contents): 
    print(contents)
    case let .failure(error):
    if let fileError = error as? FileError,
    fileError == .fileDoesNotExist { 
        print("File not found")
     } else {
     // 处理错误
     } 
}

二:抛出和捕获

Swift 内建的错误处理的实现方式和上面的例子类似, 但Swift 没有使用返回Result 的方式来表示失败,而是将方法标记为 throws。Result 是作用于类型上的,而throws 作用于函数。对于每个可以抛出的函数,编译器会验证调用者有没有捕获错误,或者把这个错误向上传递给它的调用者.

现在方法是这样样子

func contents(ofFile flename: String) throws -> String{
        //return "aaaa"
        throw FileError.fileIsEmpty(name: "aaa")
    }

使用是这样

do{
     let str = try contents(ofFile: "test.txt")
      print(str)
}catch FileError.fileDoesNotExist{
     print("文件不存在")
 }catch{
      print(error)
  }

是不是很奇特,压根没地方提到FileError,但是却可以进行匹配.
这是因为Swift 的错误抛出其实是无类型的:我们只能够将一个函数标记为 throws,但是我们并不能指定应该抛出哪个类型的错误。这是一个有意的设计,在大多数时候,你只关心有没有错误抛出。如果我们需要指定所有错误的类型,事情可能很快就会失控:它将使函数类型的签名变得特别复杂,特别是当函数调用其他的可抛出函数,并且将它们的错误向上传递的时候,这个问题将尤为严重。另外,添加一个错误类型,可能对使用这个 API 的所有客户端来说都是一个破坏性的 API 改动.
所以我们只能通过文档或者注释来说明这个方法到底抛出了什么类型的错误.

因为无类型,所以必须要把所有的情况都要匹配到,因此最后会有一个通用的catch,并且类似newValue,这里有一个error参数可以使用.

如果我们想要在错误中给出更多的信息,我们可以使用带有关联值的枚举。举个例子,如果我们想写一个文件解析器,我们可以用下面的枚举来代表可能的错误

enum ParseError: Error {
     case wrongEncoding 
      case warning(line: Int, message: String) 
}

do {
     let result = try parse(text: "{ \"message\": \"We come in peace\" }")
     print(result)
} catch ParseError.wrongEncoding { 
      print("Wrong encoding")
} catch let ParseError.warning(line, message) { 
      print("Warning at line \(line): \(message)")
} catch { }

三:Rethrows

引入一个例子,创建一个用来检查一系列文件有效性的函数。检查单个文件的checkFile 函数有三种可能的返回值。如果返回 true,说明该文件是有效的。如果返回 false,文件无效。如果它抛出一个错误,则说明在检查文件的过程中出现了问题.

func checkFile(flename: String) throws -> Bool

现在再封装一次,实现对一组文件的检查,必须全部有效才能返回true,如果报错,就把错误返回,不在这里处理,
什么都不用做,只要在外层函数上再标记一次throws即可.

func checkAllFiles(flenames: [String]) throws -> Bool { 
        for flename in flenames { 
                guard try checkFile(flename: flename) else { return false } 
        } 
        return true 
}

这个例子还有很多相似的场景,现在我们希望把它抽象出来,一个for循环加上一个条件判断,类似于map和filter.

extension Sequence {
     /// 当且仅当所有元素满⾜条件时返回 `true`
     func all(matching predicate: (Element) -> Bool) -> Bool { 
            for element in self { 
                    guard predicate(element) else { return false } 
             } 
             return true 
      } 
}

但是这个扩展方法没有处理throws,现在出现了两难的情况,如果all是throws版的,那么不需要抛出异常的场景就必须得try! all..或者do{ all.. }catch这样去处理.

因此Swift有一个rethrows,它告诉编译器,这个函数只会在它的参数函数抛出错误的时候抛出错误。对那些向函数中传递的是不会抛出错误的 check 函数的调用,编译器可以免除我们一定要使用 try 来进行调用的要求.

extension Sequence { 
      func all(matching predicate: (Element) throws -> Bool) rethrows -> Bool { 
            for element in self { 
                  guard try predicate(element) else { return false } 
            } 
            return true
       } 
}

需要抛出异常的例子

func checkFile(flename: String) throws -> Bool

func checkAllFiles(flenames: [String]) throws -> Bool { 
      return try flenames.all(matching: checkFile)
 }

不需要抛出异常的例子

func isPrime(_ a:Int) -> Bool
    
func checkPrimes2(_ numbers: [Int]) -> Bool {
     return numbers.all { isPrime($0) }
}

四:defer

在很多语言里,都有try/finally 这样的结构,其中finally 所围绕的代码块将一定会在函数返回时被执行,而不论最后是否有错误被抛出。Swift 中的 defer 关键字和它的功能类似,但是具体做法稍有不同。和finally 一样,defer 块会在作用域结束的时候被执行,而不管作用域结束的原因到底是什么,defer 与 finally 不一样的地方在于前者不需要在之前出现 try 或是 do 这样的语句,你可以很灵活地把它放在代码中需要的位置.
需要注意的是crash不会执行defer.

下面这段代码中,如果try process(fle: fle)失败,就会抛出异常并执行defer而不会return,如果不抛出异常则会在return之后执行defer.

func contents(ofFile flename: String) throws -> String {
     let fle = open("test.txt", O_RDONLY)
     defer { close(fle) } 
      let contents = try process(fle: fle)
      return contents 
}

如果相同的作用域中有多个 defer 块,它们将被按照逆序执行。行为就想一个栈,当进行到某一步抛出异常时,就会从异常代码的上面开始逆序执行,当需要一扇一扇开门的时候就需要逆序一扇一扇的关掉,下面这个例子很好的表达了这个意思.

guard let database = openDatabase(...) else { return } 
defer { closeDatabase(database) } 
guard let connection = openConnection(database) else { return } 
defer { closeConnection(connection) } 
guard let result = runQuery(connection, ...) else { return }

五:异步任务

讲了这么多,但其实在错误处理最大的场景,异步API调用的情况中,Swift的原生错误处理throws基本没什么用.

这是一个网络请求,但是没有包含异常处理

func getData(callback: (Int?) -> ())

如果我们想通过throws抛出异常,那显然不能写在computeThrows后面,因为computeThrows本身不会有异常,他就是调用服务端API,应该写在回调方法后面.
但是这样含义就变成了callback这个函数是有可能是失败的
回想前面提到的概念,可选值和 Result 作用于类型,throws作用于函数,对于异步的api,我们希望处理的是数据结果,是类型.

func computeThrows(callback: (Int) throws -> ())

因此使用Result是更合适的,思路上也很接近OC中返回complete(response : Any?, error: Error?),不过使用起来要方便多了.

func computeResult(callback: (Result<Int>) -> ())

相关文章

网友评论

      本文标题:Swift进阶十:错误处理

      本文链接:https://www.haomeiwen.com/subject/ayvjmrtx.html