<转>在Swift中构建assert()
__FILE__
和 __LINE__
这两个神奇的宏定义是C语言中偶尔有用的特性。他们被构建在预处理程序中,并在C语言语法分析程序运行前被展开。尽管Swift没有预处理程序,它却提供了名称相似的类似功能,但隐藏着极其不同的实现方式。
内建标识符
就像在the Swift programming guide中描述的那样,Swift有很多内建标识符,包括__FILE__
, __LINE__
, __COLUMN__
, 和 __FUNCTION__
。这些表达式可以在任何地方使用,并被语法分析器在源码对应的当前位置展开成字符串或整数字面量。这对手动日志非常管用,比如在退出前打印出当前位置。
然而这并不能帮助我们探索实现assert()
。如果我们这样实现断言(assert):
1 func assert(predicate : @autoclosure () -> Bool) { 2 true#if DEBUG 3 truetrueif !predicate() { 4 truetruetrueprintln("assertion failed at \(__FILE__):\(__LINE__)") 5 truetruetrueabort() 6 truetrue} 7 true#endif 8 }
上面的代码将会输出实现assert()
方法所在文件的文件/行数(file/line)位置,而不是调用者的位置信息。这并不管用。
获取调用者位置
Swift从D语言借鉴了一个非常聪明的特性:当标识符在默认参数列表中被赋值时,它们会在调用者的位置被展开。为了实现这个行为,assert()
被如下这样定义:
1 func assert(condition: @autoclosure () -> Bool, _ message: String = "", 2 truefile: String = __FILE__, line: Int = __LINE__) { 3 truetrue#if DEBUG 4 truetruetrueif !condition() { 5 truetruetruetrueprintln("assertion failed at \(file):\(line): \(message)") 6 truetruetruetrueabort() 7 truetruetrue} 8 truetrue#endif 9 }
Swift的assert()
函数第二个参数是一个供你指明的可选字符串,而第三、四个参数默认为调用者上下文的位置。这就让assert()
能默认获得调用者的原始位置。如果你想在断言顶层构建你自己的抽象层,你可以将调用者的位置传递下去。作为一个简易的例子,你可以定义一个日志和断言方法:
1 func logAndAssert(condition: @autoclosure () -> Bool, _ message: StaticString = "", 2 truefile: StaticString = __FILE__, line: UWord = __LINE__) { 3 truelogMessage(message) 4 trueassert(condition, message, file: file, line: line) 5 }
这正确的将logAndAssert()
调用者的file/line位置传递给了assert()
的实现。注意上面代码中的StaticString
,它是一个类似于String
的类型,用于存储像__FILE__
这种不受内存管理约束的字符串字面量。
除此之外,这种实用设计也用于Swift中实现高级XCTest框架,也可以在你自己实现的函数库中发挥作用。
参数的惰性计算(Lazy Evaluation)
当实现Swift的assert()
时,我们遇到的第一个挑战是没有明确的方式让一个函数接收一个表达式而不评判它。比如,我们想使用:
1 func assert(x : Bool) { 2 true#if !NDEBUG 3 truetrue/*noop*/ 4 true#endif 5 }
甚至当断言失效,应用程序将会在计算表达式时损失性能:
assert(someExpensiveComputation() != 42)
修复这种情况的一种方法是在定义断言的时候让它接收一个闭包(closure):
1 func assert(predicate : () -> Bool) { 2 true#if !NDEBUG 3 truetrueif !predicate() { 4 truetruetrueabort() 5 truetrue} 6 true#endif 7 }
正如我们想要的,只有在断言有效时才计算表达式,但它也给我们留下了一个不幸的调用语法:
assert({ someExpensiveComputation() != 42 })
我们可以用Swift的@autoclosure
属性来修复它。这个自动闭包属性可以用在函数的参数上来表明一个未经花括号修饰的表达式可以被隐式的打包成闭包并作为参数传递给函数。比如:
1 func assert(predicate : @autoclosure () -> Bool) { 2 true#if !NDEBUG 3 truetrueif !predicate() { 4 truetruetrueabort() 5 truetrue} 6 true#endif 7 }
这让你更自然的调用断言:
assert(someExpensiveComputation() != 42)
自动闭包是一个强大的特性,因为你可以有条件地计算表达式,多次计算并像使用闭包那样来使用打包的表达式。自动闭包也可以在Swift的其他地方使用。比如,实现简化逻辑运算符:
1 func &&(lhs: BooleanType, rhs: @autoclosure () -> BooleanType) -> Bool { 2 truereturn lhs.boolValue ? rhs().boolValue : false 3 }
通过将右边表达式以闭包形式接收,Swift提供合适的子表达式的惰性计算。
自动闭包
作为C语言的宏,自动闭包要谨慎使用。因为从调用函数的一方看不出来参数的计算受到了影响。自动闭包有意地限制我们不传递参数,所以你不能在类似条件控制流的情形中使用它。在符合人们期望的实用语义情况(可能是“features”API)下使用它,而不是单单为了省略闭包的花括号。
转自:玉令天下的博客