12、Swift中的异常处理

12、Swift中的异常处理

  • 1、Swift中的错误处理机制
    • 1.1 Optional
    • 1.2 通过enum和Error封装错误
    • 1.3 Swift中的throw和catch
  • 2、如何处理closure发生的错误
  • 3、NSError是如何桥接到Swift原生错误处理的
  • 4、Swift中的错误时如何映射到NSError的
    • 4.1 LocalizedError
    • 4.2 CustomNSError
  • 5、try、try?、try!的区别
  • 6、使用defer以及串联either type
    • 6.1 通过defer来计数
    • 6.2 串联多个either type

1、Swift中的错误处理机制

1.1 Optional

Optional在成功的时候,返回Value,错误的时候,返回nil。甚至,有不少Swift标准库的API就是这样做的。因此,这的确是个不错的选择。但nil只适合表达非常显而易见的错误,例如:访问Dictionary中一个不存在的Keydic[“nonExistKey”],nil就只能表示Key不存在。

但如果可能会发生的错误不止一种情况,nil的表现力就很弱了。

1.2 通过enum和Error封装错误

optional通过enum的两个case(.some和.none)表示它的两个状态,我们自然也可以用一个enum表示操作成功和失败的结果:

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

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

//定义汽车类可能出现的错误
enum CarError:Error {
    case outOfFuel
    case anchor
}

//定义一个汽车
struct Car {
    var fuelInLitre:Double
    
    func start()->Result<String> {
        guard fuelInLitre > 5 else {
            return .failure(CarError.outOfFuel)
        }
        
        return .success("Ready to go")
    }
}

有了Result类型的约定,就可以按照固定套路处理异常了:

let vw = Car(fuelInLitre: 2)
switch vw.start() {

case .success(let mesasge):
    print(mesasge)
case .failure(let error):
    if let carError = error as? CarError {
        if carError == .outOfFuel {
            print("燃料不足")
        }else if carError == .anchor{
            print("抛锚")
        }
    }else {
        print("未知错误")
    }
}

1.3 Swift中的throw和catch

//定义一个汽车
struct Car {
    var fuelInLitre:Double
    
    func start() throws ->String{
        guard fuelInLitre > 5 else {
            throw CarError.outOfFuel
        }
        
        return "Ready to go"
    }
}

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

因此,在Swift里,凡是声明中带有throws关键字的,通常都会在注释中标明这个函数有可能发生的错误。

let vw = Car(fuelInLitre: 2)
do {
    let message = try vw.start()
    print(message)
}catch CarError.outOfFuel {
    print("没有燃料了")
}catch CarError.anchor{
    print("抛锚了")
}catch {
    print("未知错误")
}

2、如何处理closure发生的错误

对于异步回调函数的错误处理方式

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))
        }
    }
}

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

3、NSError是如何桥接到Swift原生错误处理的

我们写一个OC编写的类,了解Foundation中的API是如何桥接到Swift中的Sensor,表达汽车的传感器。

// In Sensor.h
extern NSString *carSensorErrorDomain;

NS_ENUM(NSInteger, CarSensorError) {
    overHeat = 100
};

@interface Sensor: NSObject {
}

+ (BOOL)checkTemperature: (NSError **)error;
@end
// In Sensor.m
NSString *carSensorErrorDomain = @"CarSensorErrorDomain";

@implementation Sensor {
}

+ (BOOL)checkTemperature: (NSError **)error {
    double temp = 10 + arc4random_uniform(120);
    
    if ((error != NULL) && (temp >= 100)) {
        NSDictionary *userInfo = @{
            NSLocalizedDescriptionKey: NSLocalizedString(
                @"The radiator is over heat", nil),
        };
        
        *error = [NSError errorWithDomain: carSensorErrorDomain
                                     code: overHeat
                                 userInfo: userInfo];
        return NO;
    }
    else if (temp >= 100) {
        return NO;
    }
    
    return YES;
}
@end

实际上,checkTemperature的这种声明:

+ (BOOL)checkTemperature: (NSError **)error

是很多Foundation API都会采取的“套路”。**通过一个BOOL搭配NSError 来表达API可能返回的各种错误。当checkTemperature桥接到Swift后,根据SE-0112中的描述,它的签名会变成这样

func checkTemperature() throws {
    // ...
}

这里要特别说明的是,只有返回BOOL或nullable对象,并通过NSError **参数表达错误的OC函数,桥接到Swift时,才会转换成Swift原生的throws函数。并且,由于throws已经足以表达失败了,因此,Swift也不再需要OC版本的BOOL返回值,它会被去掉,改成Void。

throw出来的错误是由NSError桥接来的,我们应该如何Catch。这个问题的答案,从某种程度上说,取决于API返回的NSError是如何在OC中定义的。而按照我们现在这样的定义方式,selfCheck()会返回一个NSError,我们只能这样来catch:

do {
    try vw.selfCheck()
} catch let error as NSError 
    where error.code == CarSensorError.overHeat.rawValue {
    // CarSensorErrorDomain
    print(error.domain)
    // The radiator is over heat
    print(error.userInfo["NSLocalizedDescription"] ?? "")
}

4、Swift中的错误时如何映射到NSError的

把Swift中的Error移植到Objective-C,相对而言倒是个简单很多的事情。Swift会根据enum的名字自动生成默认的error domain,并从0开始,为每一个enum中的case设置error code。

class Car: NSObject {
    var fuelInLitre: Double

    init(fuelInLitre: Double) {
        self.fuelInLitre = fuelInLitre
    }

    // ...
}

然后,在Sensor.m中,我们先包含Swift类在Objective-C中的头文件:

#import "SwiftErrorsInOC-Swift.h"

就可以在Objective-C中使用class Car了。然后,我们定义一个全局函数startACar():

// In Sensor.m
NSObject* startACar() {
    Car *car = [[Car alloc] initWithFuel:5];
    
    NSError *err = nil;
    [car startAndReturnError: &err];
    
    if (err != nil) {
        NSLog(@"Error code: %ld", (long)err.code);
        NSLog(@"Error domain: %@", err.domain);
        
        return nil;
    }
    
    return car;
}

在上面的代码里可以看到,由于Swift中的Car.start()是一个throws方法,在OC里,它会被添加一个AndReturnError后缀,并接受一个NSError **类型的参数。然后,当err不为nil时,我们向控制台打印了start抛出的错误映射到OC的结果。
由于car对象的fuel只有5,所以这个调用是一定会产生NSError的。为了在Swift中调用这个方法,我们在Sensor.h中添加下面的声明:

// In Sensor.h
NSObject* startACar();

然后,在main.swift里,我们直接调用startACar,就能在控制台看到类似这样的结果:
请添加图片描述

在这里,自动生成的NSError对象的code是0,domain是“项目名.Swift中enum的名字”。当然,这只是最基本的映射。在Swift 3里,除了Error之外,还添加了一些新的protocol,帮助我们进一步定制自动生成的NSError对象的属性。

4.1 LocalizedError

LocalizedError在Swift中是这样定义的:

protocol LocalizedError : Error {
  /// A localized message describing what error occurred.
  var errorDescription: String? { get }

  /// A localized message describing the reason for the failure.
  var failureReason: String? { get }

  /// A localized message describing how one might recover from the failure.
  var recoverySuggestion: String? { get }

  /// A localized message providing "help" text if the user requests help.
  var helpAnchor: String? { get }
}

并且,Swift为LocalizedError中的每一个属性都提供了默认值nil,因此,你可以只定义自己需要的部分就好了。例如,对于我们的CarError来说,可以把它改成这样:

enum CarError: LocalizedError {
    case outOfFuel
}

然后,通过extension给它添加额外信息:

extension CarError: LocalizedError {
    var recoverySuggestion: String? {
        return "Switch to e-power mode"
    }
}

这样,在OC的startACar实现里,我们就可以通过访问NSError的localizedRecoverySuggestion属性来读取恢复建议了:

NSObject* startACar() {
    // ...
    if (err != nil) {
        // ...
        NSLog(@"Recovery suggestion: %@", 
            err.localizedRecoverySuggestion);
        return nil;
    }
    
    // ...
}

4.2 CustomNSError

另外一个加入到Swift的protocol是CustomNSError,我们可以通过它自定义NSError中的code / domain / userInfo。

extension CarError: CustomNSError {
    static let errorDomain = "CarErrorDomain"
    
    var errorCode: Int {
        switch self {
        case .outOfFuel:
            return -100
        }
    }
    
    var errorUserInfo: [String: Any] {
        switch self {
        case .outOfFuel:
            return [
                "LocalizedDescription":
                "U r running out of fuel"
            ]
        }
    }
}

尽管在SE-0112的约定里,errorDomain是一个computed property,但至少在XCode 8.2.1中,它只能定义成一个type property。不过想来也合理,一个NSError对象只需要一个error code就可以了,我们也没什么计算它的必要。

接下来,把startACar的定义改成这样:

NSObject* startACar() {
    // ...
    if (err != nil) {
        NSLog(@"Error domain: %@", err.domain);
        NSLog(@"Error code: %ld", (long)err.code);
        NSLog(@"Error userInfo: %@", err.userInfo);
    }
    
    // ...
}

5、try、try?、try!的区别

try : 执行函数后,如果有异常需要catch异常,如果不catch,则会抛给上层函数,如果最终还是没有处理则程序crash

try? : 使用 try?通过将错误转换为可选项来处理一个错误。是可选性的执行,不报错的时候返回正常的值.如果有异常会返回一个nil,程序不会crash.不会触发catch。

try! : 是强制解包,当抛出异常的时候也解包,导致崩溃问题。

6、使用defer以及串联either type

在很多编程语言的错误处理机制中,除了“捕获”并处理异常之外,通常还会有一个finally的环节,让我们编写无论函数的执行是否顺利,在离开函数作用域的时候一定会执行的代码。Swift中也有一个类似的机制,叫做defer,只不过,它只保证在离开它所在的作用域时,内部的代码一定会被执行。你不用把它和try…catch放在一起。

6.1 通过defer来计数

例如,我们要统计所有Car对象的启动次数,就可以这样。先在Car中添加一个静态属性:

struct Car {
    // ...
    static var startCounter: Int = 0
    // ...
}

然后,把start方法修改成这样:

/// - Throws: `CarError` if the car is out of fuel
func start() throws -> String {
    guard fuel > 5 else {
        throw CarError.outOfFuel(no: no, fuel: fuel)
    }
    
    defer { Car.startCounter += 1 }

    return "Ready to go"
}

无论start()是“抛出”了错误,还是正常返回,defer中的代码都会被执行,于是startCounter都会被加1。

因此,只要你的函数有可能因为发生错误提前返回,就可以考虑使用这种模式来进行资源清理或回收的工作。

6.2 串联多个either type

相比do…catch,它有一个用法上的缺陷。在日常的编程中,我们经常会调用多个返回Result类型的方法来完成一个复杂的操作。但这通常会导致嵌套很深的代码,我们得不断在上一个方法的.success情况里,继续后续的方法调用。于是,用不了太多方法,你就受不了自己编写的代码了。

例如,对于上一节中提到的更新Car OS的例子,我们把更新的步骤更具体的表现出来。首先,添加一个新的CarError,表示文件校验错误:

enum CarError: Error {
    case outOfFuel(no: String, fuel: Double)
    case updateFailed
    case integrationError
}

然后,为了通过编译,我们给Car添加两个函数,表示下载和校验文件,按照约定,它们都返回一个Result表示执行结果:

func downLoadPackage() -> Result<String> {
    return .failure(CarError.updateFailed)
}
    
func checkIntegration(of path: String) -> Result<Int> {
    return .failure(CarError.integrationError)
}

最后,我们来看下面这个让人痛苦的osUpdate实现方式:

func osUpdate(postUpdate: @escaping (Result<Int>) -> Void) {
    DispatchQueue.global().async {
        // 1. Download package
        switch self.downLoadPackage() {
        case let .success(filePath):
            // 2. Check integration
            switch self.checkIntegration(of: filePath) {
            case let .success(checksum):
                // 3. Do you want to continue from here?
                // ...
            case let .failure(e):
                postUpdate(.failure(e))
            }
        case let .failure(e):
            postUpdate(.failure(e))
        }
    }
}

写到这里,我们仅仅完成了两个工作,在注释的第三步,你还有勇气继续再写下去么?为了解决either type的这个问题,我们得给它添加一些扩展。其实,这个问题和我们连续使用optional变量是非常类似的。既然为了不断使用optional的非nil值,我们可以使用flatMap串联,对于Result,我们可以如法炮制一个:

extension Result {
    func flatMap<U>(transform: (T) -> Result<U>) -> Result<U> {
        switch self {
        case let .success(v):
            return transform(v)
        case let .failure(e):
            return .failure(e)
        }
    }
}

之后,我们的osUpdate就可以改成这样:

func osUpdate(postUpdate: @escaping (Result<Int>) -> Void) {
    DispatchQueue.global().async {
        let result = self.downLoadPackage()
            .flatMap {
                self.checkIntegration(of: $0)
            }
            // Chain other processes here
        
        postUpdate(result)
    }
}

然后,我们可以使用flatMap继续串联任意多个后续需要执行的方法了,如果其中某个环节发生了错误,整个流程自然就结束了。这时,调用osUpdate的代码可以改成这样:

Car(fuel: 10, no: "1").osUpdate(postUpdate: {
    switch $0 {
    case let .success(checksum):
        print("Update success: \(checksum)")
    case let .failure(e):
         if let e = e as? CarError {
            switch e {
            case .integrationError:
                print("Checksum error")
            default:
                break
            }
        }
        
        print("Update failed")
    }
})

最终,在postUpdate这个回调函数里,无论是成功还是失败,我们都可以得到osUpdate最后一步操作返回的结果。

你可能感兴趣的:(Swift,swift,开发语言,ios)