diff --git a/source/chapter2/16_Automatic_Reference_Counting.md b/source/chapter2/16_Automatic_Reference_Counting.md index 191cd416..9611913c 100644 --- a/source/chapter2/16_Automatic_Reference_Counting.md +++ b/source/chapter2/16_Automatic_Reference_Counting.md @@ -325,4 +325,148 @@ Country的构造函数调用了City的构造函数。然而,只有Country的 在上面的例子中,使用显示展开可选值的意义在于满足了两个类构造函数的需求。capitalCity属性在初始化完成后,能作为非可选值使用同事还避免了循环强引用。 -##闭包产生的循环强引用 +##闭包引起的循环强引用 + +前面我们看到了循环强引用环是在两个类实例属性互相保持对方的强引用时产生的,还知道了如何用弱引用和无主引用来打破循环强引用。 + +循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包 “捕获" self,从而产生了循环强引用。 + +循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这跟之前的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。 + +Swift提供了一种优雅的方法来解决这个问题,称之为闭包占用列表(closuer capture list)。同样的,在学习如何用闭包占用列表破坏循环强引用之前,先来了解一下循环强引用是如何产生的,这对我们是很有帮助的。 + +下面的例子为你展示了当一个闭包引用了self后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement的类,用一种简单的模型表示HTML中的一个单独的元素: + + class HTMLElement { + + let name: String + let text: String? + + @lazy var asHTML: () -> String = { + if let text = self.text { + return "<\(self.name)>\(text)" + } else { + return "<\(self.name) />" + } + } + + init(name: String, text: String? = nil) { + self.name = name + self.text = text + } + + deinit { + println("\(name) is being deinitialized") + } + + } + +HTMLElement类定义了一个name属性来表示这个元素的名称,例如代表段落的"p",或者代表换行的"br"。HTMLElement还定义了一个可选属性text,用来设置和展现HTML元素的文本。 + +除了上面的两个属性,HTMLElement还定义了一个lazy属性asHTML。这个属性引用了一个闭包,将name和text组合成HTML字符串片段。该属性是() -> String类型,或者可以理解为“一个没有参数,返回String的函数”。 + +默认情况下,闭包赋值给了asHTML属性,这个闭包返回一个代表HTML标签的字符串。如果text值存在,该标签就包含可选值text;如果text不存在,该标签就不包含文本。对于段落元素,根据text是"some text"还是nil,闭包会返回"`

some text

`"或者"`

`"。 + +可以像实例方法那样去命名、使用asHTML属性。然而,由于asHTML是闭包而不是实例方法,如果你想改变特定元素的HTML处理的话,可以用自定义的闭包来取代默认值。 + +> 注意: asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用asHTML。也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。 + +HTMLElement类只提供一个构造函数,通过name和text(如果有的话)参数来初始化一个元素。该类也定义了一个析构函数,当HTMLElement实例被销毁时,打印一条消息。 + +下面的代码展示了如何用HTMLElement类创建实例并打印消息。 + + var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") + println(paragraph!.asHTML()) + // prints"hello, world" + +>注意: 上面的paragraph变量定义为可选HTMLElement,因此我们可以赋值nil给它来演示循环强引用。 + +不幸的是,上面写的HTMLElement类产生了类实例和asHTML默认值的闭包之间的循环强引用。循环强引用如下图所示: + +![](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/closureReferenceCycle01_2x.png) + +实例的asHTML属性持有闭包的强引用。但是,闭包在其闭包体内使用了self(引用了self.name和self.text),因此闭包占有了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。(更多关于闭包占有值的信息,请参考[Capturing Values](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-XID_129))。 + +>注意: 虽然闭包多次使用了self,它只占有HTMLElement实例的一个强引用。 + +如果设置paragraph变量为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,也是因为循环强引用: + + paragraph = nil + +注意HTMLElementdeinitializer中的消息并没有别打印,证明了HTMLElement实例并没有被销毁。 + +##解决闭包引起的循环强引用 + +在定义闭包时同时定义占有列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。占有列表定义了闭包体内占有一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。 + +>注意: Swift有如下要求:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod(而不只是someProperty或someMethod)。这提醒你可能会不小心就占有了self。 + +##定义占有列表 + +占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)成对组成。每一对都在花括号中,通过逗号分开。 + +占有列表放置在闭包参数列表和返回类型之前: + + @lazy var someClosure: (Int, String) -> String = { + [unowned self] (index: Int, stringToProcess: String) -> String in + // closure body goes here + } + +如果闭包没有指定参数列表或者返回类型,则可以通过上下文推断,那么可以占有列表放在闭包开始的地方,跟着是关键字in: + + @lazy var someClosure: () -> String = { + [unowned self] in + // closure body goes here + } + +##弱引用和无主引用 + +当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。 + +相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包内检查他们是否存在。 + +>注意: 如果占有的引用绝对不会置为nil,应该用无主引用,而不是弱引用。 + +前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样这样编写HTMLElement类来避免循环强引用: + + class HTMLElement { + + let name: String + let text: String? + + @lazy var asHTML: () -> String = { + [unowned self] in + if let text = self.text { + return "<\(self.name)>\(text)" + } else { + return "<\(self.name) />" + } + } + + init(name: String, text: String? = nil) { + self.name = name + self.text = text + } + + deinit { + println("\(name) is being deinitialized") + } + + } + +上面的HTMLElement实现和之前的实现一致,只是在asHTML闭包中多了一个占有列表。这里,占有列表是[unowned self],表示“用无主引用而不是强引用来占有self”。 + +和之前一样,我们可以创建并打印HTMLElement实例: + + var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") + println(paragraph!.asHTML()) + // prints "

hello, world

" + +使用占有列表后引用关系如下图所示: + +![](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Art/closureReferenceCycle02_2x.png) + +这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果将paragraph赋值为nil,HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息。 + + paragraph = nil + // prints "p is being deinitialized"