第六章——函数(自动闭包和内存)

182 阅读5分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

@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关键字在一些调试、日志函数中也很有用, 比如我们可以看到在fatalErrorassert函数的定义中也用到了@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,这样可以简化方法调用者的工作。之前我们已经知道weakunownded关键字可以被省略,下面举一个例子说明self.也可以被省略:

class Bar {
var i = 0
func some() {
doIt {
print(i) // 如果没有@noescape,这里需要写self.i
}
}
}

默认情况下,标记为@autoclosure的闭包参数都是@noescape的。在极少部分情况下,你也可以把参数标记为@autoclosure(escaping),这表示闭包在函数作用域之外依然生效,在写异步操作的代码时可能会用到。