Swift的错误处理是该语言的独特功能。它看起来很像其他语言中的异常,但语法不完全相同,它也不尽如人意。今天我要看看Swift错误在内部工作。
语义
我们先来看一下Swift错误如何在语言层面上工作。
任何Swift函数都可以使用throws关键字进行装饰,这表明它可以引发错误:
func getStringMightFail()throws - > String {...
要从这样的一个函数中抛出错误,请使用符合Error协议的值的throw关键字:
抛出MyError.brainNotFound
调用throws函数时,必须包含try关键字:
let string = try getStringMightFail()
try关键字不执行任何操作,但它是一个必需的标记,表示该函数可能会引发错误。调用必须在允许抛出错误的上下文中,无论是在throws函数中还是在带有catch处理程序的do块中。
要写一个catch处理程序,将try调用放在一个do块中,并添加一个catch块:
做{
let string = try getStringMightFail()
...
} catch {
print(“发生错误:\(错误)”)
}
当抛出错误时,执行跳转到catch块。抛出的值可用错误。您可以通过类型检查和条件以及多个catch子句来获得想法,但这些都是基础知识。有关所有详细信息的详细信息,请参阅Swift编程语言的错误处理部分。
这就是它的作用。它是如何工作的?
履行
为了找出它是如何工作的,我写了一些虚拟代码与错误处理我可以反汇编:
struct MyError:Error {
var x:Int
var y:Int
var z:Int
}
func投掷者(x:Int,y:Int,z:Int)throws - > Int {
抛出MyError(x:x,y:y,z:z)
}
func捕手(f:(Int,Int,Int)throws - > Int){
做{
让x =尝试f(1,2,3)
print(“Received \(x)”)
} catch {
print(“Caught \(error)”)
}
}
当然,现在Swift是开源的,我可以直接看看编译器的代码,看看它的作用。但这不是乐趣,而且这更容易。
事实证明,Swift 3和Swift 4做的不一样。我将简要介绍一下Swift 3,然后再来看看Swift 4,因为这是即将到来的。
Swift 3的工作原理是自动化Objective-C的NSError约定。编译器插入一个额外的隐藏参数,这个参数基本上是Error *或NSError **。抛出错误包括将错误对象写入该参数中传递的指针。调用者分配一些堆栈空间并在该参数中传递其地址。返回时,它会检查该空间是否现在包含错误。如果是,它跳到catch块。
斯威夫特4获得了一个好心人。基本思想是一样的,但是除了正常的额外参数之外,还会为错误返回保留一个特殊的寄存器。以下是Thrower中的相关汇编代码:
调用imp___stubs__swift_allocError
mov qword [rdx],rbx
mov qword [rdx + 8],r15
mov qword [rdx + 0x10],r14
mov r12,rax
这将调用Swift运行时分配一个新的错误,用相关的值填充它,然后把指针放在r12中。然后它返回给呼叫者。 Catcher中的相关代码如下所示:
叫r14
mov r15,rax
测试r12,r12
je loc_100002cec
它进行呼叫,然后检查r12是否包含任何内容。如果是,它跳到catch块。 ARM64上的技术几乎相同,x21寄存器用作错误指针。
在内部,它看起来很像返回Result类型,或者返回某种错误代码。 throws函数在特殊的地方将调用者的抛出错误返回给调用者。调用者检查该位置是否存在错误,如果是,则跳转到错误处理代码。生成的代码看起来类似于使用NSError **参数的Objective-C代码,实际上Swift 3的版本是相同的。
与异常比较
在讨论其错误处理系统时,Swift非常谨慎,不要使用“异常”这个词,但它看起来很像其他语言的例外。它的实现如何比较?有很多语言有例外,其中许多语言做的不同,但自然的比较是C ++。 Objective-C异常(尽管它们确实存在,尽管没人使用它们)在现代运行时使用C ++的异常机制。
充分探索C ++异常如何工作可以填写一本书,所以我们必须解决一个简短的描述。
调用throw函数(这是C ++函数的默认值)的C ++代码正好像调用非抛出函数一样生成程序集。也就是说,它传递参数并检索返回值,没有考虑例外的可能性。这怎么可能工作?除了生成无异常代码之外,编译器还生成一个表,其中包含有关代码如何(以及是否)处理异常的信息,以及如何在抛出异常的情况下如何安全地展开堆栈以退出函数。当某些功能引发异常时,它会爬起堆栈,查找每个函数的信息,并使用它来将堆栈解压缩到下一个函数,直到找到异常处理程序或运行结束。如果它找到一个异常处理程序,它将控制权转移到该处理程序,然后在catch块中运行代码。有关C ++异常如何工作的更多信息,请参阅C ++ ABI for Itanium:异常处理。该系统被称为“零成本”异常处理。术语“零成本”是指当没有抛出异常时会发生什么。因为该代码的编译完全与没有例外一样,因此不支持异常的运行时开销。调用潜在投掷函数与调用不抛出函数一样快,并且将try块添加到代码中并不会在运行时完成任何额外的工作。当抛出异常时,“零成本”的概念出现在窗口之外。使用表解开堆栈是一个昂贵的过程,需要大量的时间。该系统围绕着很少抛出异常的想法进行设计,并且在没有抛出异常的情况下的性能更重要。这个假设在几乎所有的代码中都可能是真实的。相比之下,Swift的系统非常简单。它不会为throws和non-throws函数生成相同的代码。相反,每次调用throws函数后都要检查是否返回错误,如果是这样,则跳转到相应的错误处理代码。这些检查不是免费的,虽然它们应该很便宜。对于Swift来说,权衡是非常有意义的。 Swift错误看起来很像C ++异常,但实际上它们的用法不一样。几乎任何C ++调用都可能会抛出,甚至像新操作符这样的基本的东西也会被抛出来,以指示一个错误。在每次调用之后,明确地检查抛出的异常将添加大量额外的检查。相比之下,几个Swift电话在典型的代码库中被标记为throws,因此显式检查的成本很低。结论Swift的错误处理请求与其他语言(例如C ++)中的异常进行比较。 C ++的异常处理在内部非常复杂,但Swift采取不同的方法。 Swift在常见的情况下,不用放松表来实现“零成本”,而是在特殊寄存器中返回抛出的错误,而调用者检查该寄存器以查看是否已经抛出错误。当错误不被抛出时,这增加了一些开销,但是避免了使C ++做的事情变得非常复杂。编写Swift代码将会非常努力,错误处理的开销会产生明显的差异。