Swift Param Attribute - @autoclosure why?

2015-06-10 18:10:19 +08:00
 banxi1988

我在基本了解了 Swift 中 assert 方法的使用之后,冒出来的问题是: 为什么要用 @autoclosure
而不是一个直接的布尔类型.

为什么要用 @autoclosure?

第一次看到 @autoclosure 是在 assert 方法中看到的.
它要达到的目的是类似 C 语言中的 assert 宏.

那首先来看 C 语言中 assert 宏是什么样的

C 语言中的 assert

C 语言中的assert定义在标准库中的assert.h头文件中
我先用伪代码来表示一下:

#如果是非调试模式
  #定义 assert(e) 不做事情
#否则即是调试模式
  #定义 assert(e) 如果 表达式 e 求值结果 为true 的话,不做任何事情 ,否则 报告错误(错误信息,包含当前源文件,行号,方法名,参数)

实际代码可以参考 : bionic的实现 assert.h

#ifdef NDEBUG
    #define assert(e) ((void)0)
#else
    #define assert(e) ((e) ? (void)0 : __assert2(__FILE__, __LINE__,__func__,#e))
#endif

由于C语言有预编译的特性. 使用了assert调用.如果是使用 RELEASE 模式编译的话,
assert的调用都变成了 ((void)0) 都变成了空.
调试模式下,当结果不为真时,打印一下错误信息.

但是Swift这样的现代语言没有预编译,和宏展开.

Swift中参数惰性求值问题

在Swift中遇到这到一个问题.原话如下:

When implementing assert() in Swift, the first challenge we encounter is that there is no obvious way for a function to accept an expression without evaluating it. For example, say we tried to use

例如下面的使用场景:

func fileAtPath(path:String)->Bool{
    print("\(__FUNCTION__)")
    return NSFileManager.defaultManager().fileExistsAtPath(path)
}

assert(fileAtPath("/tmp/nofile"))

上面的使用场景中,如果是C语言的assert调用的话,在RELEASE模式下,assert语句整个都被擦除了.

但是在Swift中已经没有这样的宏展开. 所以上面assert方法还会被调用.
那如果Swift中的 assert的实现是如下的话:

func assert(boolValue:Bool){
   #if !NDEBUG  // 如果是调试模式
   if !boolValue{
    // 报告错误
    abort() // 终止程序执行
   }
   #endif

}

关键的时,fileAtPath("/tmp/nofile") 会先被调用. 但是这个fileAtPath的参数表达式的执行在 RELEASE模式下是没有意义的了.
因为assert在RELEASE模式下相当于一个空函数.

总结: 如果不经验特殊的处理. Swift中的 assert调用,在 RELEASE模式下也会导致,参数表达式执行开销.

@autoclosure 属性的引入

于是Swift,尝试将实现改为接受一个闭包而不是 Bool类型的参数.
如下:

func assert(predicate:() -> Bool){
  #if !NDEBUG // 如果是调试模式
    if !predicate(){
      // 报告错误
      abort() // 终止程序执行
    }
  #endif
}

接收一个闭包的话,在RELEASE模式下,predicate闭包不会执行,也即其中表达式不会被执行. 避免了不必要的语句执行开销.

但是这有一个问题就是, assert的使用将变得不方便了.
如果像上面声明的话,我们需要像下面这样写调用assert方法

assert({ fileAtPath("/tmp/nofile") })

写起来复杂,理解起来也复杂,反正是增加了复杂性.及使用难度.

Swift 通过 引入了编译属性 autoclosure来解决这个问题.
顾名思义的话,即自动闭包. 自动帮我们生成闭包?
这样assert函数的实现改为如下:

func assert(predicate:@autoclosure () -> Bool){
  #if !NDEBUG // 如果是调试模式
    if !predicate(){
      // 报告错误
      abort() // 终止程序执行
    }
  #endif
}

调用简化如下:

assert(fileAtPath("/tmp/nofile"))

于是问题得到解决, 好写,并且,效率不受影响.

@autoclosure Swift 语言中的 其他使用场景

  1. 逻辑与或中的表达式短路执行 逻辑与的特点是: 如果前面一个表达式求值为false的话,后面的表达式就不用求值了. 于是实现可能是如下代码:
func &&(lhs: BooleanType, rhs: @autoclosure () -> BooleanType) -> Bool{
     return lhs.boolValue ? rhs().boolValue: false
 }

逻辑或的特点是: 如果前面的一个表达式求值为true的话,后面的表达式就不用求值了. 于是实现大概是如下代码:

func ||(lhs:BooleanType, rhs: @autoclosure () -> BooleanType) -> Bool{
    return lhs.boolValue ? true: rhs().boolValue()
}
  1. 在 ?? 操作符 (Nil Coalescing Operator) 中的使用 a ?? b的执行效果相当于 a != nil ? a! : b 这个语法糖的声明如下:
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T

    func ??<T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?

于是上面的方法的实现细节可能如下, from http://swifter.tips/autoclosure/

func ??<T>(optional : T?, defaultValue: @autoclosure () -> T) -> T{
   switch optional{
      case .Some(let value):
            return value
        case .None:
            return defaultValue()
   }
 }

@autoclosure 虽好,但不要乱用

因为 : 对应方法的调用都,可能不知道 @autoclosure对应的表达式,可能不会被调用. 因为它看起来就是一个会被要执行的表达式.

Swift 标准库中的 @autoclosure使用

Swift中对 @autoclosure的使用主要是如下的函数

func precondition(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = default, line: UWord = default)

@noreturn func preconditionFailure(@autoclosure message: () -> String = default, file: StaticString = default, line: UWord = default)

@noreturn func fatalError(@autoclosure message: () -> String = default, file: StaticString = default, line: UWord = default)

func assertionFailure(@autoclosure message: () -> String = default, file: StaticString = default, line: UWord = default)

func assert(@autoclosure condition: () -> Bool, @autoclosure _ message: () -> String = default, file: StaticString = default, line: UWord = default)

func &&<T : BooleanType>(lhs: T, rhs: @autoclosure () -> Bool) -> Bool
func ||<T : BooleanType, U : BooleanType>(lhs: T, @autoclosure rhs: () -> U) -> Bool

func ??<T>(optional: T?, @autoclosure defaultValue: () -> T?) -> T?
func ??<T>(optional: T?, @autoclosure defaultValue: () -> T) -> T

上面的几个方法: assert类,上面讨论得比较多了,
值得注意的是: precondition类, 如果不满足其条件的话,在 RELEASE模式下也会导致应用停止运行.

一个简单的使用示例 :

func checkTrue(@autoclosure condition:() -> Bool, @autoclosure _ message:() -> String = "assertion failed"){
    if !condition(){
        print(message())
    }
}

checkTrue(true)

参考

  1. Building assert() in Swift, Part 1: Lazy Evaluation
  2. http://swifter.tips/autoclosure/
  3. Nil Coalescing Operator ?? 操作符
3644 次点击
所在节点    iDev
3 条回复
lizhuoli
2015-06-16 10:32:47 +08:00
这个写的不错,Swift的Assert本身确实不是非常方便。@autoclosure还没有细看,感谢了。
mailworks
2015-12-20 16:26:12 +08:00
赞。
ChouJum
2017-12-07 09:48:21 +08:00
感谢,@autoclosure 的存在解疑答惑了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/197563

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX