Friday Q&A 2016-03-04:Swift 断言
断言是一种非常有用的机制,它可以检查代码中的假设部分,确保错误能够被及时发现。今天我将探讨 Swift 中提供的断言调用以及它们的实现,这个话题是由读者 Matthew Young 提出的。 我不会花太多时间讨论一般意义上的断言是什么或者在哪里使用它们。本文将着眼于 Swift 中提供的断言机制以及一些实现的细节。如果你想要了解如何在代码中充分利用断言,可以阅读我以前的文章 Proper Use of Asserts(断言的正确使用)。 API在 Swift 标准库中有两个主要的断言函数。 第一个函数被创造性地命名为 assert(x >= 0) // x 不能为负 该函数提供一个可选参数,用于命题为假时打印错误信息: assert(x >= 0,"x can't be negative here")
有些人倾向仅在调试版本中使用断言,理论上调试的时候去做一些检查是个好习惯,但最好保证 app 不会在实际使用时崩溃。不管在断言检查中有没有出现过,一旦(在实际使用中)出现错误,都会导致非常严重的后果。更好的做法是,如果在实际使用时出现错误,应用能迅速退出。我们来看一下如何实现。 函数 precondition(x >= 0) // x 不能为负 precondition(x >= 0,"x can't be negative here") 不同之处在于该函数在优化构建条件下也会执行检查。这使得它成为断言检查的一个更好的选择,并且检查速度足够快。 尽管 关于非检查构建有趣的一点是,尽管 这些函数各自有一个不带条件的变体,用来标志失败的情况。上述两个函数的变体分别是 guard case .Thingy(let value) = someEnum else { preconditionFailure("This code should only be called with a Thingy.") } 优化下的行为和带条件时类似,开启优化时 最后还有个函数 记录调用者信息当断言检查未通过,会得到这样一条信息:
程序是如何获知文件和代码行的信息的呢? 在 C 语言中,我们将 c #define assert(condition) do { if(!(condition)) { fprintf(stderr,"Assertion failed %s in file %s line %dn",#condition,__FILE__,__LINE__); abort(); } } 这些函数最终以调用者的文件和代码行信息结尾,就是因为此处的宏定义。Swift 中没有宏的概念,那该怎么办? Swift 中可以使用默认参数值达到同样效果。上述神奇的标识符可被当做参数的默认值使用。如果调用者没有提供一个确切的值,便可将调用者所处的文件及代码行作为默认值。目前,这两个神奇的标识符分别是 探讨实际中的使用前,我们先看看 public func assert( @autoclosure condition: () -> Bool,@autoclosure _ message: () -> String = String(),file: StaticString = #file,line: UInt = #line ) 通常情况下,调用 没有强制要求必须使用默认值,如果需要的话你可以传入其他的值。比如: assert(false,"Guess where!",file: "not here",line: 42) 最终输出:
有种更加实用的用法,你可以写一个包装器来保留原始调用者的信息,例如: func assertWrapper( @autoclosure condition: () -> Bool,line: UInt = #line ) { if !condition() { print("Oh no!") } assert(condition,message,file: file,line: line) } Swift 版的 自动闭包上述函数都使用 先快速回顾一下 func f(@autoclosure value: () -> Int) { print(value()) } f(42) 等价于: func f(value: () -> Int) { print(value()) } f({ 42 }) 为什么要把表达式包装成闭包传递?因为这样可以让调用的函数来决定表达式具体执行的时间。例如,对于实现两个布尔类型的 && 运算符时,我们可以通过传入两个 func &&(a: Bool,b: Bool) -> Bool { if a { if b { return true } } return false } 有些情况下我们直接调用就可以: x > 3 && x < 10 但如果右操作数计算复杂的话是很耗时的: x > 3 && expensiveFunction(x) < 10 假定左操作数为 optional != nil && optional!.value > 3 跟 C 语言一样,Swift 中的 func &&(a: Bool,@autoclosure b: () -> Bool) -> Bool { if a { if b() { return true } } return false } 现在就符合 Swift 的语义了,当 a 为 false 时 b 永远不会执行。 对断言而言,则完全是考虑性能问题。因为断言消息有可能是很耗时的操作。例如: assert(widget.valid,"Widget wasn't valid: (widget.dump())") 你肯定不想每次都去计算一长串字符串,即便 条件本身也是 assert(superExpensiveFunction()) 本文提到的 API 中的函数都使用了 代码移除基于代码的编译情况,这些函数会在代码生成时被移除。它们位于 Swift 标准库,而不是你自己写的代码中,而 Swift 标准库的编译远早于你自己的代码。这一切是怎么协调的? 在 C 语言中,这一切都跟宏相关。宏仅存在于头部,因此会在执行代码行的时候编译,尽管原则上这些代码隶属于库,实际上它们直接被当做你自己的代码。这意味着它们可以检查是否设置了 c #if DEBUG #define assert(condition) do { if(!(condition)) { fprintf(stderr,__LINE__); abort(); } } #else #define assert(condition) (void)0 #endif 又一次,在 Swift 中没有宏的概念,那是怎么做的呢? 如果你看过这些函数在标准库中的定义,会发现它们都用 标准库是一个独立的库,独立库中的函数是怎么内联进你自己的代码中的呢?对 C 语言来讲,库中包括编译对象的代码,这个问题显得没有意义。 Swift 标准库是一个
这意味着这些断言函数的函数体被以一种中间形式保存到标准库模块中。之后调用函数的时候函数体内的代码便可被内联。既然可以被内联,这些代码也就处于同一编译环境下,必要时优化器也可以将它们全部移除。 总结Swift 提供了一系列好用的断言函数。 这些函数对 断言函数是标准库的一部分,但它们使用了 今天就讲到这里!希望这篇文章可以帮助你在自己的代码中更大胆地使用断言。断言是很有用的机制,它可以让问题一旦发生就及时明显地显现出来,而不是发生很久后才显示出一些“症状”。下次会带来一些更棒的想法。每周周五问答都是基于读者的一些想法建立的,如果你也有想在这里讨论的话题,就快发过来吧!
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |