美文网首页
NSError是如何桥接到Swift原生错误处理的?

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

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

    使用Swift编程,一个不可避免的问题,就是和使用Objective-C编写的各种API打交道,这些API大多通过返回NSError表达错误信息。当我们进行混编的时候,NSError是如何与Swift原生的Error类型进行交互的呢?

    NSError是如何桥接到Error的?

    ,我们给之前的项目添加一个用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
    
    

    我们只添加了一个检测水温的方法checkTemperature:error,为了模拟不同水温的情况,它的实现里,我们只是随机生成了一个10-130之间的随机数,并在温度超过100摄氏度时,返回NSError

    // 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 {
        // ...
    }
    
    

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

    理解了这个桥接过程后,我们给Car添加一个自检的方法:

    struct Car {
        // ...
    
        func selfCheck() throws {
            try Sensor.checkTemperature()
        }
    }
    
    

    这里,由于checkTemperature是一个throws方法,我们调用的时候要使用try关键字。并且,由于没有使用do...catchcheckTemperature返回的错误就会被“扔”到selfCheck里,因此,它也得是一个throws方法,我们可以用这种方式不断向上一级“抛出”错误。

    如果我们一直这样“抛出”错误,最终,错误就会被传到Swift运行时默认的错误处理方法,结果,当然就是你的App闪退了。

    接下来,在调用start()方法前,我们先对Car对象进行自检:

    do {
        try vw.selfCheck()
    
        // ...
    }
    
    

    现在,问题来了。既然selfCheck()有可能“抛出”错误,但这个错误是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"] ?? "")
    }
    
    

    虽然可以正常工作,但你看到了,为了匹配到selfCheck的错误,我们得先进行一步类型转换,再检查转换结果的code是否相等,这显然太啰嗦了。为什么不能把CarSensorError变成一个可以直接catch的对象,并让它包含OC中所有错误信息呢?

    按照SE-0112中的设计,CarSensorError的确应该是可以直接catch的。为此,Objective-C中还专门加了一个宏:NS_ERROR_ENUM。按照这份proposal中的定义,这个宏会在Swift中引入一个和OC ENUM同名的结构,这个结构中包含了和NSError中相同的信息。

    但在Xcode 8.2.1中,这个宏却还不能正常使用。为了试验(SE-0112)中定义的行为,我们得自己把它添加进来。

    在Sensor.h中,添加下面的宏定义:

    // In Sensor.h
    #if __has_attribute(ns_error_domain)
        #define NS_ERROR_ENUM(type, name, domain) \
            _Pragma("clang diagnostic push") \
            _Pragma("clang diagnostic ignored \"-Wignored-attributes\"") \
            NS_ENUM(type, __attribute__((ns_error_domain(domain))) name) \
            _Pragma("clang diagnostic pop")
    #else
        #define MY_ERROR_ENUM(type, name, domain) NS_ENUM(type, name)
    #endif
    
    

    由于Swift 2.3中,ns_error_domain并没有任何语义,因此,我们先进行了兼容性判断,仅在Swift 3的环境中定义了NS_ERROR_DOMAIN,本质上,这就是一个特殊的NS_ENUM。在NS_ENUM前后的三个_Pragma用于在处理宏定义时,临时关闭警告,它们并不影响编译结果。

    我们定义的NS_ERROR_ENUM有三个参数:

    • typeENUM中值的类型;
    • nameENUM类型的名字;
    • domain:指定error domain的值;

    这样,我们就可以定义一个专门表示NSError code的ENUM

    // In Sensor.h
    NS_ERROR_ENUM(NSInteger, CarSensorError, carSensorErrorDomain) {
        overHeat = 100
    };
    
    

    然后,Swift就会导入一个叫做CarSensorErrorstruct,它兼容了NSError中的所有信息。这样,我们就能用下面两种方式来“捕获”Car.selfCheck返回的错误了。

    第一种方式只能识别对应的NSError,这种方式语法上最简单,但会丢掉一些信息:

    do {
        try vw.selfCheck()
    } catch CarSensorError.overHeat {
        print("The radiator is over heat")
    }
    
    

    可以看到,这样和Swift原生的Error类型用起来完全没区别。但是,除了code之外,我们就无法读到对应的domainuserInfo信息了。

    第二种方式,是直接把error转型为CarSensorError,这样不但可以直接识别错误,还可以保留所有的NSError信息:

    do {
        try vw.selfCheck()
    } catch let error as CarSensorError {
        print(error._domain)
        print(error.errorCode)
        print(error.userInfo["NSLocalizedDescription"] ?? "")
    }
    
    

    对于Swift引入的CarSensorError结构的定义,大家去看下SE-0112中关于实现细节的描述就明白了。

    相关文章

      网友评论

          本文标题:NSError是如何桥接到Swift原生错误处理的?

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