美文网首页
理解Swift中的错误处理机制

理解Swift中的错误处理机制

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

    如何利用Swift的语言机制表达错误呢?你可能最先想到的就是optional。成功的时候,返回Value,错误的时候,返回nil。甚至,有不少Swift标准库的API就是这样做的。因此,这的确是个不错的选择。但nil只适合表达非常显而易见的错误,例如:访问Dictionary中一个不存在的Keydic["nonExistKey"]nil就只能表示Key不存在。

    但如果可能会发生的错误不止一种情况,nil的表现力就很弱了,我们很难知道究竟什么原因导致了错误。怎么办呢?

    通过enum和Error封装错误

    如果你还记得optional的实现方式,就会发现这个问题很好解决。既然optional通过enum的两个case.some.none)表示它的两个状态,我们自然也可以用一个enum表示操作成功和失败的结果:

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

    为了能包含不同成功结果,Result得是一个泛型enum。而Error则是Swift中的一个protocol,它没有任何具体的约定,只用来表示一个类型的身份。稍后,我们会看到,只有遵从了Error的错误,才可以被throw

    有了这个enum之后,怎么用呢?假设我们要定义一个表示汽车的struct

    struct Car {
        var fuelInLitre: Double
    
        func start() -> Result<String> {
            guard fuelInLitre > 5 else {
                // How we express the error here?
            }
    
            return .success("Ready to go")
        }
    }
    
    

    我们设定只有当燃料大于5升的时候,才可以正常启动。start方法通过返回Result<String>表达了这个概念。当条件满足时,我们直接返回.success("Ready to go")。然而,我们该如何处理.failure的情况呢?

    首先,要根据我们有可能遇到的错误,再自定义一个遵从Errorenum

    enum CarError: Error {
        case outOfFuel
    }
    
    

    目前,我们只有一个燃料不足的情况,因此,先定义一个case就好了。然后,在start的实现里,燃料不足时,我们直接返回CarError.outOfFuel

    func start() -> Result<String> {
        guard fuel > 5 else {
            return .failure(CarError.outOfFuel)
        }
    
        // ...
    }
    
    

    然后,有了Result类型的这种约定,我们就可以采用固定的套路来处理错误:

    let vw = Car(fuel: 2)
    
    switch vw.start() {
    case let .success(message):
        print(message)
    case let .failure(error):
        if let carError = error as? CarError,
            carError == .outOfFuel {
            print("Cannot start due to out of fuel")
        }
        else {
            print(error.localizedDescription)
        }
    }
    
    

    case .failure里,我们可以通过对error类型转换的结果来判断是否发生了特定的错误,进而进行专门的处理。而对于所有不识别CarError,我们可以直接打印Swift提供的一个本地化的描述。

    理解Swift中的throw和catch

    理解了这种处理错误的思路之后就会发现,它哪都好,唯一不好的地方在于我们所有的约定都只能停留在口头上,Result的名字也好,.success.failure的名字也好,都是如此。我们无法避免有人使用诸如ResultType.succ / .fail这样的名字,于是,不同的开发者写出的代码可能思路都是一样的,却无法搭配在一起好好工作。

    解决这个问题唯一的办法,就是能按照上面的思路,提供一种编程语言层面的保障。而这,就是throw关键字的用途。我们的start方法可以改成这样:

    /// - Throws: `CarError` if the car is out of fuel
    func start() throws -> String {
        guard fuel > 5 else {
            // How we press the error here?
            // return .failure(CarError.outOfFuel)
            throw CarError.outOfFuel
        }
    
        // return .success("Ready to go")
        return "Ready to go"
    }
    
    

    相比之前的版本,throws版本有了下面这些改进:

    • 通过throws关键字表示一个函数有可能发生错误相比Result更加统一和明确。并且,通过throws,函数可以恢复返回正确情况下要返回的类型;
    • 遇到错误的情况时,通过throw关键字表示“抛出”一个“异常情况”,它有别于使用return返回正确的结果;

    如果你有过其他面向对象编程语言的经验,对这种写法可能更为熟悉。但就像我们在注释中对比的那样,在Swift中throw一个Errorreturn .failure(...)这种写法是没有任何区别的,“抛出”的错误没有明确的类型,这种“异常”也不会带来任何运行时成本。throw就是一个语法糖而已。

    因此,在Swift里,凡是声明中带有throws关键字的,通常都会在注释中标明这个函数有可能发生的错误。否则,我们很难知道该如何处理。为此,Swift还在markdown注释中添加了一类关键字,就像我们对start的注释一样。

    除了在表达方式上更为统一之外,使用throws声明函数还有一个好处,就是编译器会强制我们用“标准”的方法来调用可能会发生错误的函数。因此,我们之前的start调用就可以变成这样:

    do {
        let message = try vw.start()
        print(message)
    } catch CarError.outOfFuel {
        print("Cannot start due to out of fuel")
    } catch {
        print("We have something wrong")
    }
    
    

    可以看到,当我们调用start()时,要明确使用try关键字表示这种调用是个尝试,它有可能失败。然后,对于这种调用,我们必须把它包含在一个do...catch里,其中,每个catch用来匹配start有可能会返回的一种错误。最后,我们用一个不匹配任何具体Errorcatch表示匹配其他未列出的错误。虽然这并不是必要的,但是一旦start()返回了我们没有catch的错误,就会导致运行时错误。

    同样,要再次强调的是,这里的do...catch也是个语法糖,它和Java中的try...catch只是语法类似。而本质上,do...catch和我们之前使用的switch...case是没有任何区别的。

    相关文章

      网友评论

          本文标题:理解Swift中的错误处理机制

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