Swift 之 @auto_closure
用 C 实现一个 assert()
,通常是这么做的:
1 |
assert 就是断言,这里采用条件编译,作用是如果在调试情况下,检查参数 e,如果是 false,就给出错误提示并终止程序执行,如果是非 DEBUG 情况下,就什么都不做。这种宏实现的方式是没有运行时性能影响的,因为我们知道宏展开基本是直接替换的,没有对表达式求值的过程。
比如这样简单的一个宏,用来返回两个数中的较大值:
1 |
当我们使用的时候,比如 MAX(10, 20)
, 宏展开后的结果是 (10 >= 20 ? 10 : 20)
, 而不是计算到最终的结果 20
. 但是在方法调用中,参数值是直接求值的,比如我们有个判断一个数是否偶数的函数:
1 | func isEven(num : Int) -> Bool { |
当我们调用 isEven(10 + 20)
的时候,先计算 10 + 20
的结果,然后把 30
作为参数传递到 isEven
函数中。
OK. 在 Swift 里也实现了这样一个功能的 assert () 函数,而且没有用到宏 (你骗人,明明用到了啊?!, 就是#if !NDEBUG
啊。 好吧,相信苹果 Swift 官方 Blog 在下一篇文章中应该会有相应的机制来判断当前的环境的,这里的意思是没用宏来实现表达式的延迟求值。),是怎么实现的呢?
首先在 Swift 里没有办法写一个函数,它接受一个表达式作为参数,但是却不执行它。比如,我们想这么实现:
1 | func assert(x : Bool) { |
然后这么用:
1 | assert(someExpensiveComputation() != 42) |
我们发现,总是要计算一遍表达式 someExpensiveComputation() != 42
的值,是真是假,然后把这个值传递到 assert 函数中。即便我们在非 Debug 的情况下编译也是一样,那怎么样条件执行呢,像上面的使用宏的方式,当条件满足的时候才对表达式求值?还是有办法的,就是修改这个方法,把参数类型改为一个闭包,像这样:
1 | func assert(predicate : () -> Bool) { |
然后调用的时候创建一个匿名闭包,然后传给 assert 函数:
1 | assert({ someExpensiveComputation() != 42 }) |
这样当我们禁用 assert 的时候,表达式 someExpensiveComputation() != 42
就不会被计算,减少了性能上的消耗,但是显而易见,调用的代码就显的不那么清爽优雅了。
于是乎 Swift 引入了一个新的 @auto_closure
属性,它可以用在函数的里标记一个参数,然后这个参数会先被隐式的包装为一个 closure,再把 closure 作为参数给这个函数。好绕啊,直接看代码吧,使用 @auto_closure, 上面的 assert 函数可以改为:
1 | func myassert(predicate : @auto_closure () -> Bool) { |
然后我们就可以这么调用了:
1 | assert(someExpensiveComputation() != 42) |
哇。好神奇!
仔细看一下 myassert () 函数的参数:
1 | predicate : @auto_closure () -> Bool |
predicate 加上了 @auto_closure 的属性,后面是个 closure 类型 () -> Bool
。其实 predicate 还是 () -> Bool
类型的,只是在调用者可以传递一个普通的值为 Bool 表达式,,然后 RunTime 会自动把这个表达式包装为一个 () -> Bool
类型的闭包作为参数传给 myassert () 函数,简而言之就是中间多了一个由表达式到闭包的自动转换过程。
@auto_closure
的功能非常强大和实用,有了它,我们就可以根据具体条件来对一个表达式求值,甚至多次求值。在 Swift 的其他地方也有 @auto_closure
的身影,比如实现短路逻辑操作符时,下面是 &&
操作符的实现:
1 | func &&(lhs: LogicValue, rhs: @auto_closure () -> LogicValue) -> Bool { |
如果 lhs 已经是 false 了,rhs 也就没有必要计算了,因为整个表达式肯定为 false。这里使用 @auto_closure
就轻松实现了这个功能。
最后,正如宏在 C 中的地位一样,@auto_closure
的功能也是非常强大的,但同样应该小心使用,因为调用者并不知道参数的计算被影响 (推迟) 了。@auto_closure
故意限制 closure 不能有任何参数(比如上面的 () -> Bool
),这样我们就不会把它用于控制流中。
编译自 Swift 的官方 Blog Building assert() in Swift, Part 1: Lazy Evaluation 一文