Swift 学习笔记(@autoclosure 与 call-by-name)
咋一眼看到 Swift @autoclosure 自动闭包便不由自主的联想到 Scala 的 Call-By-Name 求值策略。既然主要话题是关于 Swift 的,那还是 Swift 的 @autoclosure 先入为主,下面例子中的
对
同时我们依据闭包的简化规则倒退发现:给任何一个表达式两边加上大括号 {},就构成了一个闭包,它是无参数的闭包,返回值可有可无。例如 {1+2}, {foo(10)+5}, {print("Hello")}, {array.removeAtIndex(0)} 等等
至此,我们还未死心,思考能不能对 greet{"Swift"} 的写法进一步简化,这就要请出 @autoclosure 隆重登场, 对 greet() 的 name 参数前加上 @autoclosure, 就是
有了 @autoclosure 修饰之后,就像调用普通函数一样调用 greet(_:) ,直接传入字符, 看起来这个函数接收的是一个字符串。实质上在 @autoclosure 的作用下参数会被自动装箱成一个闭包, 也就把表达式映射到闭包过程由函数来完成。即 greet("Swift") 相当于 greet({"Swift"}), 但这个例子中有 @autoclosure 修饰的情况下是不能写成 greet({"Swift"}), 否则产生了二次装箱变成了 greet({{"Swift"}})
若说 @autoclosure 使得传入的值被延迟估值(延迟到函数中对闭包调用时求值)是不太恰当的,其本质是闭包固有的延迟估值属性, @autoclosure 的本质是帮你完成了表达式到闭包的映射,简单来讲就是用 @autoclosure 一次性的替换了每次调用时两边的大括号。
我们来扯 @autoclosure 达成了表达式的延迟估值有点跑偏,所以我们这里说闭包的延迟估值,且看示例
如果非闭包参数,我们知道求值是发生成函数调用前
关于闭包参数和延迟求值特性, 如果闭包有条件的被执行,并且表达式求值耗资源的话可以好好利用这一特性。
Apple 官方说 @autoclosure 可能会使得代码不易于理解,应在上下文或函数名中暗示出传入的参数会被延迟求值的 . 这里的顾虑也就是延迟求值可能会带来费解与潜在的 Bug, 比如
如果我们未仔细了解 foo(_:) 函数的原型,不清楚它的参数是 @autoclosure 的,我们会迷惑为什么前面 index++ 了, print(index) 输出的还是 0, 而我们在循环中经常会 foo(index++) 这么调用
顺便提一句,@autoclosure 默认为 @noescape 属性,如果想要 @autoclosure 为 escaping 属性,注解就合一块写成 @autoclosure(escaping)
既然本文标题中提到了 Scala 的 Call-By-Name 估值策略,那么还是以一个 Scala 的例子作为最生动的说明
执行
✗ scala Test.scala
1:
2: World
Scala 不需要像 Swift 的
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
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) 进行许可。