Swift 学习笔记(@autoclosure 与 call-by-name)

咋一眼看到 Swift @autoclosure 自动闭包便不由自主的联想到 Scala 的 Call-By-Name 求值策略。既然主要话题是关于 Swift 的,那还是 Swift 的 @autoclosure 先入为主,下面例子中的 greet(name: () -> String) 接收一个闭包作为参数

1func greet(name: () -> String) {
2    print("Hello \(name())")
3}
4
5greet({() in return "Swift"}) //标准
6greet({return "Swift"})       //简化
7greet({"Swift"})              //再简化
8
9greet{"Swift"}  //因为 greet 只有一个参数,因而可以这样写

greet(_:) 的函数调用不断简化,最后简化为 greet{"Swift}。总而言之大括号是甩不了了,有大括号的围绕才叫闭包。

同时我们依据闭包的简化规则倒退发现:给任何一个表达式两边加上大括号 {},就构成了一个闭包,它是无参数的闭包,返回值可有可无。例如 {1+2}, {foo(10)+5}, {print("Hello")}, {array.removeAtIndex(0)} 等等

至此,我们还未死心,思考能不能对 greet{"Swift"} 的写法进一步简化,这就要请出 @autoclosure 隆重登场, 对 greet() 的 name 参数前加上  @autoclosure, 就是
1func greet(@autoclosure name: () -> String) {
2    print("Hello \(name())")
3}
4
5greet("Swift")

有了 @autoclosure 修饰之后,就像调用普通函数一样调用 greet(_:) ,直接传入字符, 看起来这个函数接收的是一个字符串。实质上在 @autoclosure 的作用下参数会被自动装箱成一个闭包, 也就把表达式映射到闭包过程由函数来完成。即 greet("Swift") 相当于 greet({"Swift"}), 但这个例子中有 @autoclosure 修饰的情况下是不能写成 greet({"Swift"}), 否则产生了二次装箱变成了 greet({{"Swift"}})

若说 @autoclosure 使得传入的值被延迟估值(延迟到函数中对闭包调用时求值)是不太恰当的,其本质是闭包固有的延迟估值属性, @autoclosure 的本质是帮你完成了表达式到闭包的映射,简单来讲就是用 @autoclosure 一次性的替换了每次调用时两边的大括号。

我们来扯 @autoclosure 达成了表达式的延迟估值有点跑偏,所以我们这里说闭包的延迟估值,且看示例
 1var fruits = ["Apple", "Pear"]
 2let closure = {fruits.removeAtIndex(0)}  //大括号框起来就是个闭包
 3
 4print(fruits)    //["Apple", "Pear"]
 5closure()
 6print(fruits)    //["Pear"]
 7
 8func gonogo(flag: Bool, @autoclosure value: ()-> String) {
 9    if flag { value() }     //闭包真正调用时才是闭包求值时
10}
11
12print(fruits)    //["Pear"]
13gonogo(false, value: fruits.removeAtIndex(0))  //false, value 未被求值
14print(fruits)    //["Pear"]
15gonogo(true, value: fruits.removeAtIndex(0))   //true, value 被求值
16print(fruits)    //[]

如果非闭包参数,我们知道求值是发生成函数调用前
 1func gonogo(flag: Bool, value: String) { //参数都是在调用前被求值
 2    if flag { value }
 3}
 4
 5var fruits = ["Apple", "Pear"]
 6print(fruits)    //["Apple", "Pear"]
 7gonogo(false, value: fruits.removeAtIndex(0)) //不管 true 或 false, 先对 value 表达式求值
 8print(fruits)    //["Pear"]
 9gonogo(true, value: fruits.removeAtIndex(0)) //不管 true 或 false, 先对 value 表达式求值
10print(fruits)    //[]

关于闭包参数和延迟求值特性, 如果闭包有条件的被执行,并且表达式求值耗资源的话可以好好利用这一特性。

Apple 官方说 @autoclosure 可能会使得代码不易于理解,应在上下文或函数名中暗示出传入的参数会被延迟求值的 . 这里的顾虑也就是延迟求值可能会带来费解与潜在的 Bug, 比如
1let flag = false
2func foo(@autoclosure value: ()->Int) {
3    if flag { value() }
4}
5
6var index = 0
7foo(index++)
8print(index)  //输出 0

如果我们未仔细了解 foo(_:) 函数的原型,不清楚它的参数是 @autoclosure 的,我们会迷惑为什么前面 index++ 了, print(index) 输出的还是 0, 而我们在循环中经常会 foo(index++) 这么调用

顺便提一句,@autoclosure 默认为 @noescape 属性,如果想要 @autoclosure 为 escaping 属性,注解就合一块写成 @autoclosure(escaping)

既然本文标题中提到了 Scala 的 Call-By-Name 估值策略,那么还是以一个 Scala 的例子作为最生动的说明
 1val buffer = new StringBuilder
 2
 3def callByName(flag: Boolean, value: => StringBuilder) {  //与日常的 Call-By-Value 的区别就是冒号与类型之前多了一个 =>
 4  if(flag)  value
 5}
 6
 7callByName(false, buffer ++= "Hello")    //因为 value 是 =>StringBuiler 类型,所以  buffer ++= "Hello" 会包装为一个闭包
 8println("1: " + buffer)                  //1:
 9
10callByName(true, buffer ++= "World")     //与上同理
11println("2: " + buffer)                  //2: World

执行

✗ scala Test.scala
1:
2: World

Scala 不需要像 Swift 的 @autoclosure 那样的注解,只需要把原来的参数声明 value: StringBuilder 中间加个 => 变成 value: => StringBuilder,就实现了 Swift 的 @autoclosure 的特性 永久链接 https://yanbin.blog/swift-learning-autoclosure-call-by-name/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。