本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。
@autoclosure关键字
我们都很熟悉&&
运算符,它是一个短路运算符。它有两个操作数,首先左边的操作数被处理,判断是不是true
,只有当它为true
时才会继续判断右边的操作数是不是true
。这是因为根据&&
运算符的特性,如果左边的操作数是true
,整个表达式的值才有可能为true
。如果左边的运算数为false
,我们就短路这个运算,右边的运算数也不会被访问到。
比如,我们要对数组的第一个元素进行某个判断,代码可以这样写:
var evens = [1,2,3]
if !evens.isEmpty && evens[0] > 10 {
// 对evens[0]进行一些处理
}
这种写法依赖于&&
运算符的短路机制,否则的话如果evnes
为空数组,程序就会崩溃。
虽然几乎所有语言的&&
、||
运算符都有内建的的短路机制,但如何为自定义的运算符添加短路机制呢。试想一下,如果我们这样实现&&
运算符:
func and(l: Bool, r: Bool) -> Bool {
guard l else { return false }
return r
}
这样的and
方法不具备短路机制。因为被传入的参数r
的类型是Bool
类型的,也就是类似于evens[0] > 10
这样的判断,在and
方法内部已经得出了结果。但我们希望的是把这个判断过程延迟,只在参数l
的值为true
时才进行。这启发我们用闭包来实现这个需求,因为闭包可以把真正要执行的代码封装到一个变量中并延迟执行:
func and(l: Bool, _ r: () -> Bool ) -> Bool {
guard l else { return false }
return r()
}
and
方法应该这样调用:
if and(!evens.isEmpty, {evens[0] > 10}) {
// 对evens[0]进行一些处理
}
与&&
运算符相比,在and
方法中,第二个参数变成了闭包,但其实我们关心的只是其中的内容:evens[0] > 10
。使用闭包延迟了计算的发生,却也给写代码带来了一些麻烦,有没有什么办法既能延迟计算,又能书写简单呢?这时就需要@autoclosure
关键字出马了。
从字面来看,它表示“自动闭包”,也就是说它可以把一个表达式自动变成闭包的形式,我们修改一下and
方法的定义,把闭包参数定义为@autoclosure
:
func and(l: Bool, @autoclosure _ r: () -> Bool ) -> Bool {
guard l else { return false }
return r()
}
于是and
方法的调用就可以略作简化了:
if and(!evens.isEmpty, evens[0] > 10) {
// 对evens[0]进行一些处理
}
除了可以用于短路运算符外,@autoclosure
关键字在一些调试、日志函数中也很有用, 比如我们可以看到在fatalError
和assert
函数的定义中也用到了@autoclosure
关键字。以assert
函数为例,它只在调试时才会触发,因此在Release
模式下,闭包内的代码就不会执行。这样有助于提高程序的效率。
@noescape关键字
使用闭包时我们需要格外注意内存管理问题,在使用捕获列表时,我们有可能需要把self
标记为weak
,这样闭包就不会持有对self
的强引用。不过在使用类似于map
这样的函数时,我们从来不会把任何截获的变量标记为weak
,这是因为map
是同步执行,这个闭包参数也不会被别的对象引用,所以不会产生循环引用问题。我们还看一下map
函数的第一个参数,它被标记为@noescape
:
@noescape transform: (Self.Generator.Element) throws -> T
@noescape
表示闭包的作用域不会超出map
函数。一旦map
函数执行结束,闭包就不会再被引用。也就是说,编译器确保不会在异步调用的回调函数中使用这个闭包,也不会有全局变量或对象的属性持有这个闭包。这样一来,编译器可以对闭包做一些微小的优化,方法的调用者不用担心内存管理问题,我们可以像平时那样写代码,没有捕获列表和weak
关键字,访问方法调用者的属性时也不要加上self.
。
举个例子来看一下,我们首先定义两个函数,这两个函数的参数code
都是被标记成@noescape
的闭包:
func doIt(@noescape code: () -> ()) {}
func doItMore(@noescape code: () -> ()) {}
在doIt
函数内部,有些针对code
的操作是允许的,有些操作是不允许的:
func doIt(@noescape code: () -> ()) {
/* 下面的三行代码都是可行的 */
code() // 直接调用这个闭包,当然是可以的
doItMore(code) // 把它作为参数,传入另一个函数doItMore中也是可以的。前提是doItMore函数中把参数声明为@noescape
doItMore {
code() // 在被标记为@noescape的闭包中截获code闭包也是可以的
}
/* 下面的三行代码会导致编译错误 */
// code作为参数传到了另一个函数中,而dispatch_async函数并没有把他的闭包参数定义为@noescape
dispatch_async(dispatch_get_main_queue(), code)
let _code:() -> () = code // 不能用变量来存储@noescape闭包
let __code = { code() } // 不能在没被标记成@noescape的闭包中截获code闭包
}
总的来说,@noescape关键字定义的是闭包参数的使用规则,而非这个闭包自身如何实现。因为闭包参数不能被临时存储,也不能被异步调用,所以这就决定了它的作用域只局限于函数内部。
在我们自己实现一个接收闭包作为参数的函数时,如果可能的话应该把这个闭包标记为@noescape
,这样可以简化方法调用者的工作。之前我们已经知道weak
和unownded
关键字可以被省略,下面举一个例子说明self.
也可以被省略:
class Bar {
var i = 0
func some() {
doIt {
print(i) // 如果没有@noescape,这里需要写self.i
}
}
}
默认情况下,标记为@autoclosure
的闭包参数都是@noescape
的。在极少部分情况下,你也可以把参数标记为@autoclosure(escaping)
,这表示闭包在函数作用域之外依然生效,在写异步操作的代码时可能会用到。