美文网首页
如何处理closure参数会发生的错误?

如何处理closure参数会发生的错误?

作者: 醉看红尘这场梦 | 来源:发表于2020-03-14 19:57 被阅读0次

    在Swift里,通过一个函数类型的参数来定制函数的行为我们已经很熟悉了,它可以帮助我们对不同层次的业务逻辑进行更清晰的分层和抽象。但一直以来,我们使用的closure都有一个特点,就是它们不会“抛出”错误。但情况并不总是如此,当函数类型的参数有可能返回错误时,针对不同的应用场景,会有不同的处理方法。

    同步执行的closure参数

    为了了解它们的用法,我们先给Car一个表示编号的属性:

    struct Car {
        var fuelInLitre: Double
        var no: String
    
        // ...
    }
    
    

    然后,把燃料不足的错误添加一些详细信息:

    enum CarError: Error {
        case outOfFuel(no: String, fuelInLitre: Double)
    }
    
    

    这样,我们就可以知道车辆具体的燃料情况了。最后,我们把车辆启动的方法进行相应的修改,并把检测项目改成调用start()

    /// - Throws: `CarError` if the car is out of fuelInLitre
    func start() throws -> String {
        guard fuelInLitre > 5 else {
            throw CarError.outOfFuel(no: no, fuelInLitre: fuelInLitre)
        }
    
        return "Ready to go"
    }
    
    func selfCheck() throws -> Bool {
        _ = try start()
    
        return true
    }
    
    

    现在,假设我们有1000台车等待启动检测:

    var vwGroup: [Car] = []
    
    (1...1000).forEach {
        let amount = Double(arc4random_uniform(70))
        vwGroup.append(Car(fuelInLitre: amount, no: "Car-\($0)"))
    }
    
    

    这里,我们生成了一个包含1000个Car对象的Array,并用随机数给其中的每台车加了油。接下来,为了逐台启动车辆进行检测,我们给Sequence添加了一个extension

    extension Sequence {
        func checkAll(by rule:
            (Iterator.Element) -> Bool) -> Bool {
    
            for element in self {
                guard rule(element) else { return false }
            }
    
            return true
        }
    }
    
    

    想法很简单,通过一个接受Array元素类型,并返回Bool的函数检查Array中的每一个元素,只有所有元素都返回true时,最终结果才是true,否则就返回false。在这个例子里,函数参数rulecheckAll来说,就是我们说到的同步执行的closure。

    但当我们尝试执行checkAll的时候,却会遇到点儿小麻烦:

    _ = vwGroup.checkAll(by: {
        try $0.selfCheck()
    })
    
    

    编译器会提示我们不能把一个throws方法用在一个不会“抛出”错误的closure里,怎么办呢?我们得把checkAll方法做一些修改。第一步,当然是让closure参数可以“抛出错误”:

    func checkAll(by rule:
        (Iterator.Element) throws -> Bool) -> Bool {
    
        for element in self {
            // Still error here
            guard try rule(element) else { return false }
        }
    
        return true
    }
    
    

    但这样仍旧不行,编译器会提示我们没有在checkAll的实现里,处理try rule(element)返回的错误。因此,我们还需要一种方法,表达“只有当rule抛出错误时,checkAll才会抛出错误”这样的概念,而这,就是Swift中rethrows关键字的用途:

     func checkAll(by rule:
        (Iterator.Element) throws -> Bool) rethrows -> Bool {
    
        for element in self {
            guard try rule(element) else { return false }
        }
    
        return true
    }
    
    

    这样就表示,checkAll自身的实现是安全的,调用它是否安全,取决于传递给它的closure参数。至此,处理同步调用closure参数返回错误的问题就搞定了,我们可以用下面代码进行检测下:

    do {
        _ = try vwGroup.checkAll(by: {
            try $0.selfCheck()
        })
    }
    catch let CarError.outOfFuel(no, fuelInLitre) {
        print("\(no) is out of fuel. Current: \(fuelInLitre)L")
    }
    
    

    就会在控制台看到类似"Car-5 is out of fuelInLitre. Current: 1.0L"这样的提示了。

    异步执行的closure参数

    接下来,我们看另外一个场景,函数的closure参数是被异步调用的,在这种情况下,我们之前的方案就不那么好用了。为了演示这个场景,我们给Car添加一个osUpdate方法:

    struct Car {
        // ...
        func osUpdate(postUpdate: @escaping (Int) -> Void) {
            DispatchQueue.global().async {
                // Some update staff
                let checksum = 200
    
                postUpdate(checksum)
            }
        }
    }
    
    

    它有一个closure参数postUpdatepostUpdate接受一个整数参数,表示更新后的校验码。这里为了示意,我们只是硬编码了200。由于更新涉及到下载,解压缩和文件IO,我们把它放在了一个单独的线程中完成,等操作完成之后,我们我们调用postUpdate方法通知调用者。因此,在这个例子里,postUpdate就是我们提到的异步执行的closure。

    然后,我们用下面的代码尝试更新一台车的OS:

    vwGroup[0].osUpdate(postUpdate: {
        if $0 == 200 {
            print("Starting Car OS...")
        }
    })
    
    sleep(1)
    
    

    如果没有错误,我们就能在控制台看到Starting Car OS...的提示了。但通常,实际的情况并不这么简单,在更新的过程中,网络有可能中断、下载有可能失败、文件IO有可能发生错误。因此,我们不一定可以得到一个校验码。为了表达错误,一个最简单的方式,就是让postUpdate接受一个Int?

    func osUpdate(postUpdate: @escaping (Int?) -> Void) {
        // ...
    }
    
    

    然后,我们就要这样来更新系统:

    vwGroup[0].osUpdate(postUpdate: {
        if let checksum = $0, checksum == 200 {
            print("Starting Car OS...")
        }
    })
    
    

    但显然,一个nil远不足以表达在更新过程中可能发生的各种错误。但这时,如果我们像之前一样把closure参数变成throws,也同样不解决问题:

    func osUpdate(postUpdate: @escaping (Int) throws -> Void) {
        // ...
    }
    
    

    这表示,postUpdate自身会“抛出”错误,而并不是说它接受一个可以表示错误结果的参数。于是,你可能会想,那我就把throws再往里挪一层呗:

    enum CarError: Error {
        case outOfFuel(no: String, fuelInLitre: Double)
        case updateFailed
    }
    
    func osUpdate(postUpdate: 
        @escaping (() throws -> Int) -> Void) {
            DispatchQueue.global().async {
                // Some update staff
                let checksum = 200
    
                postUpdate {
                    if checksum != 200 { 
                        throw CarError.updateFailed
                    }
    
                    return checksum
                }
            }
        }
    
    

    虽然在语法上这并无不妥,当你把他写出来之后,自己可能都会觉得并不那么容易理解。然而,对于osUpdate的使用者来说,他们的感受同样糟糕:

    vwGroup[0].osUpdate(postUpdate: {
        (getResult: (() throws -> Int)) in  
        do {
            let checksum = try getResult()
        }
        catch CarError.updateFailed {
            print("Update failed")
        }
        catch {
        }
    })
    
    

    想必你从未在Apple官方的API中有过如此的开发体验。因此,在异步回调函数中处理错误,也是Swif原生的错误处理机制目前还无法优雅处理的问题。但是,如果我们去掉错误处理机制的语法糖,用一开始的Result封装一下执行结果,你立刻就能找回似曾相识的感觉:

    func osUpdate(postUpdate: @escaping (Result<Int>) -> Void) {
        DispatchQueue.global().async {
            // Some update staff
            let checksum = 400
    
            if checksum != 200 {
                postUpdate(.failure(CarError.updateFailed))
            }
            else {
                postUpdate(.success(checksum))
            }
        }
    }
    
    

    相比之前的实现,Result版本的逻辑要简单直观的多。这时,osUpdate用起来,也会有似曾相识的感觉:

    vwGroup[0].osUpdate(postUpdate: {
        switch $0 {
        case let .success(checksum):
            print("Update success: \(checksum)")
        case let .failure(error):
            print(error.localizedDescription)
        }
    })
    
    

    因此,对于异步回调函数的错误处理方式,这样的解决方案也得到了Swift开源社区的认同。很多第三方框架都使用了类似的解决方案。对于Result<T>,由于包含了两类不同的值,它也有了一个特别的名字,叫做either type。

    相关文章

      网友评论

          本文标题:如何处理closure参数会发生的错误?

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