Swift 2 throws 全解析 - 从原理到实践
本文最初于 2015 年 12 月发布在 IBM developerWorks 中国网站发表,其网址是 http://www.ibm.com/developerworks/cn/mobile/mo-cn-swift/index.html。 作者:王巍 (@onevcat),一名来自中国的 iOS / Unity 开发者。现居日本,就职于 LINE。正在修行,探求创意之源。 感谢王巍授权转载。 Swift 2 错误处理简介throws 关键字和异常处理机制是 Swift 2 中新加入的重要特性。Apple 希望通过在语言层面对异常处理的流程进行规范和统一,来让代码更加安全,同时让开发者可以更加及时可靠地处理这些错误。Swift 2 中所有的同步 Cocoa API 的 func copyItemAtPath(_ srcPath: String,toPath dstPath: String,error: NSErrorPointer)
而在 Swift 2 中,变为了 throws: throws 使用时,Swift 1.x 中我们需要创建并传入 let fileManager = NSFileManager.defaultManager()
在实践中,因为这个 API 仅会在极其特定的条件下 (比如磁盘空间不足) 会出错,所以开发者为了方便,有时会直接传入 nil 来忽视掉这个错误: NSFileManager.defaultManager()
这种做法无形中降低了应用的可靠性以及从错误中恢复的能力。为了解决这个问题,Swift 2 中在编译器层级就对 throws 进行了限定。被标记为 throws 的 API,我们需要完整的 do {
? ?try fileManager.copyItemAtPath(srcPath,toPath: dstPath)
} catch let error as NSError { ? ?// 发生了错误
? ?print(error.localizedDescription)
}
对于非 Cocoa 框架的 API,我们也可以通过声明 throws 技术内幕throws 关键字究竟做了些什么,我们可以用稍微底层一点的手法来进行一些探索。 Swift 编译器,SIL 及汇编所有的 Swift 源文件都要经过 Swift 编译器编译后才能执行。Swift 编译过程遵循非常经典的 LLVM 编译架构:编译器前端首先对 Swift 源码进行词法分析和语法分析,生成 Swift 抽象语法树 (AST),然后从 AST 生成 Swift 中间语言 (Swift Intermediate Language,SIL),接下来 SIL 被翻译成通用的 LLVM 中间表述 (LLVM Intermediate Representation,LLVM IR),最后通过编译器后端的优化,得到汇编语言。整个过程可以用下面的框图来表示: Swift 编译器提供了非常灵活的命令行工具:swiftc,这个命令行工具可以运行在不同模式下,我们通过控制命令行参数能获取到 Swift 源码编译到各个阶段的结果。使用 > swiftc --help ... MODES: ?-emit-sil ? ? ? ?Emit canonical SIL file(s) ?-emit-ir ? ? ? ? Emit LLVM IR file(s) ?-emit-assembly ? Emit assembly file(s) (-S) ... 在 Swift 开源之前,将源码编译到各个阶段是探索 Swift 原理和实现方式的重要方式。即使是在 Swift 开源后的今天,在面对一段代码时,想要知道编译结果和底层的行为,最快的方式还是查看编译后的语句。我们接下来将会分析一段简单的 throw 代码,来看看 Swift 的异常机制到底是如何运作的。 throw,try,catch 深层解析为了保持问题的简单,我们定义一个最简单的 // throw.swift
使用 swiftc 将其编译为 SIL: swiftc -emit-sil -O -o ./throw.sil ./throw.swift 在输出文件中,可以找到 // throw.throwMe (Swift.Bool) throws -> Swift.Bool sil hidden @_TF5throw7throwMeFzSbSb : ? ? ? ? ?$@convention(thin) (Bool) -> (Bool,@error ErrorType) { bb0(%0 : $Bool): ?debug_value %0 : $Bool ?// let shouldThrow ? ? ?// id: %1 ?%2 = struct_extract %0 : $Bool,#Bool.value ? ? // user: %3 ?cond_br %2,bb1,bb2 ? ? ? ? ? ? ? ? ? ? ? ? ? ?// id: %3 bb1: ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// Preds: bb0 ?... ?throw %4#0 : $ErrorType ? ? ? ? ? ? ? ? ? ? ? ? // id: %7 bb2: ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// Preds: bb0 ?... ?return %9 : $Bool ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // id: %10 }
throwMe(shouldThrow: Bool) -> (Bool,ErrorType)
它其实是返回的是一个 这就是说,我们想要探索 throw 的话,还需要更深入一层。用 swiftc 将源代码编译为 LLVM IR: swiftc -emit-ir -O -o ./throw.ir ./throw.swift 结果中 define hidden i1 @_TF5throw7throwMeFzSbSb(i1,?%swift.refcounted* nocapture readnone,%swift.error** nocapture) #0 { } 这是我们非常熟悉的形式,参数中的 int __TF5throw7throwMeFzSbSb(int arg0) { ? ?rax = arg0; ? ?var_8 = rdx; ? ?if ((rax & 0x1) == 0x0) { ? ? ? ? ? ?rax = 0x1; ? ?} ? ?else { ? ? ? ? ? ?rax = swift_allocError(0x1000011c8,__TWPO5throw7MyErrorSs9ErrorTypeS_); ? ? ? ? ? ?var_18 = rax; ? ? ? ? ? ?swift_willThrow(rax); ? ? ? ? ? ?rax = var_8; ? ? ? ? ? ?*rax = var_18; ? ?} ? ?return rax; } 函数最终的返回是一个 int,它有可能是一个实际的整数值,也有可能是一个指向错误地址的指针。这和 Swift 1 中传入 我们在了解了 throw 的底层机理后,对于 try_apply %15(%16) : $@convention(thin) (Bool) -> (Bool,@error ErrorType),normal bb1,error bb9 // id: %17 bb1(%18 : $Bool): ... bb9(%80 : $ErrorType): ... 其他层级的实现也与此类似,都是对返回值进行类型判断,然后进入不同的条件分支进行处理。 ErrorType 和 NSErrorthrow 语句的作用对象是一个实现了 public protocol ErrorType {
}
这个接口有一个 extension,但是也没有公开的内容: extension ErrorType {
}
我们可以通过使用 LLDB 的类型检索来获取关于这个接口的更多信息。在调试器中运行 (lldb) type lookup ErrorType protocol ErrorType { ?var _domain: Swift.String { get } ?var _code: Swift.Int { get } } extension ErrorType { ?var _domain: Swift.String { ? ?get {} ?} } 可以看到这个接口实际上需要实现两个属性:domain 描述错误的所属域,code 标记具体的错误号,这和传统的 虽然 Cocoa/CocoaTouch 框架中的 throw API 抛出的都是 InvalidUser._code: 0InvalidUser._domain: ModuleName.MyErrorInvalidPassword._code: 1InvalidPassword._domain: MyError
这虽然为按照错误号来处理错误提供了可能性,但是我们在实践中应当尽量依赖 enum case 而非错误号来对错误进行辨别,这可以提高稳定性,同时降低维护的压力。除了 enum 以外,struct 和 class 也是可以实现 throws 的一些实践异步操作中的异常处理带有 throw 的方法现在只能工作在同步 API 中,这受限于异常抛出方法的基本思想。一个可以抛出的方法实际上做的事情是执行一个闭包,接着选择返回一个值或者是抛出一个异常。直接使用一个 throw 方法,我们无法在返回或抛出之前异步地执行操作并根据操作的结果来决定方法行为。要改变这一点,理论上我们可以通过将闭包的执行和对结果的操作进行分离,来达到“异步抛出”的效果。假设有一个同步方法可以抛出异常: syncFunc<A,R>(arg: A) R
通过为其添加一次调用,可以将闭包执行部分和结果判断及返回部分分离: (arg: A)() R
这相当于将原来的方法改写为了: (arg: A) -> (Void throws -> R)
这样,单次对 asyncFunc<A,R>(arg: A,callback: (Void throws -> R) -> Void) { ?
绕了一大个圈子,我们最后发现这么做本质上其实和简单地使用 异常处理的测试在 XCTest 中暂时还没有直接对 Swift 2 异常处理进行测试的方法,如果想要测试某个调用应当/不应当抛出某个异常的话,我们可以对 XCTest 框架的方法进行一些额外但很简单包装,传入 block 并运行,然后在 try 块或是 catch 块内进行 XCTAssert 的断言检测。在 Apple 开发者论坛有关于这个问题的更详细的讨论,完整的示例代码和使用例子可以在这里找到。 类型安全的异常抛出Swift 2 中异常另一个严重的不足是类型不安全。throw 语句可以作用于任意满足 事实上从我们之前对 throw 底层实现的分析来看,在语言层面上实现只抛出某一特定类型的错误并不是很困难的事情。但是考虑到与 异常的调试和断点Swift 的异常抛出并不是传统意义的 exception,在调试时抛出异常并不会触发 Exception 断点。另外,throw 本身是语言的关键字,而不是一个 symbol,它也不能触发 Symbolic 类型的断点。如果我们希望在所有 throw 语句执行的时候让程序停住的话,需要一些额外的技巧。在之前 throw 的汇编实现中,可以看到所有 throw 语句在返回前都会进行一次
参考资料
全文完。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |