众所周知,闭包的上下文捕获是 Swift 的一大特色,不能不品尝。
但很多人,不知为何,总是坚持着一个原则:什么都看,就不看官方文档。
其实官方文档对于闭包及上下文捕获有多处、互见的叙述,比如:
Capturing Values: 闭包捕获(隐式)的简单例子docs.swift.org Capture List: 显式闭包捕获的用法和原理docs.swift.org Strong Reference Cycles for Closures: 闭包循环引用docs.swift.org Escaping Closures: 逃逸闭包docs.swift.org第 2 篇中的两个例子最为精彩,分别用显示、隐式的方式,捕获了值类型、引用类型的变量。如果能深入理解一下这两个例子背后的原理,你就能对值类型、引用类型有更好的了解。
我之前将这两例中的第 2 例变形之后,加到了面试题里,结果很多候选人都凉了。
一问才知,在[]
里,他们只写过weak self
。
倘若除了weak
外,你还知道unowned self
,那也还能及格;不知道的话,至少在 Swift 语言这方面,候选人最高只能是基础水平了。
我们可以先看例 2,再看例 1。
例 2:
class SimpleClass {
var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
print(x.value, y.value)
}
x.value = 10
y.value = 10
closure()
// Prints "10 10"
闭包显式捕获了 x
,带来的结果是:若 x
的值发生改变,闭包的运行结果也会改变;而 y
也一样。
关于这个例子,苹果指出:
There are two things namedx
in the code below, a variable in the outer scope and a constant in the inner scope, but they both refer to the same object because of reference semantics.
下文代码(对我们来说是上例)中,存在两个名叫x
的东西,一个是闭包外的变量,一个是闭包内的常量,但由于他们是引用类型,所以都指向同一个对象。
当我们「显式」地使用捕获列表 [x]
时,我们「隐式」地声明了一个新的量,一个名为 x
的常量,它的作用域是闭包内。它的显式写法是:
let closure = { [x = x] in
print(x.value, y.value)
}
或者我们也可以把它写成:
let closure = { [常量X = x] in
print(常量X.value)
print(x.value, y.value)
}
因此这里发生了一次声明、赋值的操作。任何的赋值操作,我们都可以认为是一次「复制」。
对引用类型来说,你像这样赋值,是复制了一次「x
所指向的对象」的引用,因此,x
和 常量X
就指向了同一个对象;而对于值类型来说,你可以认为,一复制,整个对象就都被复制过去了。
再看例 1:
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}
a = 10
b = 10
closure()
// Prints "0 10"
由于 [a]
相当于 [a = a]
,加上 Int
是值类型,所以闭包中的 a
就是对原 a
的复制。所以当外部的 a
发生变化,内部的 a
是不会受影响的。
我们也可以把闭包写成:
let closure = { [aCopy = a] in
print(aCopy, a, b)
}
这样的话最终就会输出 "0 10 10"
。