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: Car(fuel: 10, no: "1").osUpdate(postUpdate: {
switch {
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")
}
})
)
}
// Chain other processes here
postUpdate(result)
}
}
然后,我们可以使用flatMap继续串联任意多个后续需要执行的方法了,如果其中某个环节发生了错误,整个流程自然就结束了。这时,调用osUpdate的代码可以改成这样:
最终,在postUpdate这个回调函数里,无论是成功还是失败,我们都可以得到osUpdate最后一步操作返回的结果。