模型化对象及其关系的生命周期
Swift 使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着在 Swift 中内存管理“自动完成”,你不需要自己考虑内存管理。当这些实例不再需要时,ARC 会自动释放这些类实例所使用的内存。
然而,在少数情况下,ARC 需要更多关于代码部分之间关系的信息,以便为您管理内存。本章将描述这些情况,并展示如何使 ARC 能够管理您应用程序的所有内存。在 Swift 中使用 ARC 与使用 Objective-C 的 Transitioning to ARC Release Notes 中描述的方法非常相似。
引用计数仅适用于类的实例。结构体和枚举是值类型,而不是引用类型,不会通过引用存储和传递。
ARC 工作原理
每当创建一个类的新实例时,ARC 会分配一块内存来存储该实例的信息。这块内存包含了实例类型的信息,以及与该实例相关的任何存储属性的值。
此外,当实例不再需要时,ARC 会释放该实例所占用的内存,以便将这些内存用于其他用途。这确保了当实例不再需要时,类实例不会占用内存空间。
然而,如果 ARC 释放了一个仍在使用的实例,就无法再访问该实例的属性,或调用该实例的方法。实际上,如果你尝试访问该实例,你的应用很可能崩溃。
为了确保实例在仍需使用时不会消失,ARC 会跟踪当前引用每个类实例的属性、常量和变量的数量。只要实例还存在至少一个活动引用,ARC 就不会释放该实例。
为了实现这一点,每当将一个类实例分配给一个属性、常量或变量时,该属性、常量或变量就会对该实例进行强引用。这种引用被称为“强”引用,因为它牢牢地保持对该实例的控制,只要该强引用存在,就不会允许该实例被释放。
ARC 在行动
这里有一个自动引用计数工作的示例。这个示例从一个简单的类 Person
开始,该类定义了一个存储常量属性 name
:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person
类有一个初始化器,它设置实例的 name
属性并打印一条消息以表示初始化正在进行中。 Person
类还有一个析构器,在类的实例被释放时打印一条消息。
下一个代码片段定义了三个类型为 Person?
的变量,这些变量用于在后续代码片段中设置对新 Person
实例的多个引用。由于这些变量是可选类型( Person?
,而不是 Person
),因此它们会自动初始化为 nil
的值,并且当前不引用任何 Person
实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
现在你可以创建一个新的 Person
实例并将其赋值给这三个变量之一:
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
请注意,在调用 Person
类的初始化器时会打印消息 "John Appleseed is being initialized"
。这表明初始化已经完成。
因为新创建的 Person
实例已被赋值给 reference1
变量,所以现在 reference1
到新 Person
实例之间存在一个强引用。由于至少存在一个强引用,ARC 会确保保留这个 Person
并防止其被释放。
如果将同一个 Person
实例赋值给两个或更多的变量,就会建立两个或更多的强引用到该实例:
reference2 = reference1
reference3 = reference1
现在,这个单一的 Person
实例有了三个强引用。
如果通过将 nil
赋值给两个变量来断开其中两个强引用(包括原始引用),那么只剩下一个强引用, Person
实例不会被释放:
reference1 = nil
reference2 = nil
ARC 只有在断开第三个也是最后一个强引用时才会释放 Person
实例,在此之后可以确定你不再使用 Person
实例:
reference3 = nil
// Prints "John Appleseed is being deinitialized"
类实例之间的强引用循环
在上面的示例中,自动引用计数(ARC)能够追踪到您创建的新 Person
实例的引用数量,并在不再需要该 Person
实例时对其进行释放。
然而,有可能编写代码,使得类的实例永远不会达到强引用数量为零的情况。这种情况可能会发生在两个类实例之间持有对方的强引用,使得每个实例都保持对方的存活。这种情况被称为强引用循环。
您可以通过将类之间的一些关系定义为弱引用或非拥有所引用,而不是强引用,来解决强引用循环。此过程在“解决类实例之间的强引用循环”中进行了描述。然而,在学习如何解决强引用循环之前,了解这种循环是如何产生的很有用。
这是一个不小心创建强引用循环的示例。此示例定义了两个名为 Person
和 Apartment
的类,它们分别表示一个公寓楼及其居民:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每 Person
实例都有一个 name
属性,类型为 String
,以及一个可选的 apartment
属性,初始值为 nil
。 apartment
属性是可选的,因为一个人可能并不总是有公寓。
同样,每个 Apartment
实例都有一个类型为 unit
的 String
属性,并且有一个可选的 tenant
属性,初始值为 nil
。租户属性是可选的,因为公寓不一定总是有租户。
这两个类还定义了一个析构函数,该函数打印出该类实例正在被析构的事实。这使您可以查看 Person
和 Apartment
的实例是否如预期那样被释放。
下一个代码片段定义了两个名为 john
和 unit4A
的可选类型变量,稍后将设置为特定的 Apartment
和 Person
实例。这两个变量的初始值为 nil
,因为它们是可选的:
var john: Person?
var unit4A: Apartment?
你现在可以创建一个特定的 Person
实例和 Apartment
实例,并将这些新实例分配给 john
和 unit4A
变量:
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
创建并分配这两个实例之后,强引用如下所示。 john
变量现在有一个对新 Person
实例的强引用,而 unit4A
变量有一个对新 Apartment
实例的强引用:
现在可以将这两个实例链接起来,使得人有公寓,公寓有人住。请注意,使用感叹号( !
)来解包并访问存储在 john
和 unit4A
可选变量中的实例,以便可以设置这些实例的属性:
john!.apartment = unit4A
unit4A!.tenant = john
创建链接这两个实例之后,强引用如下所示:
不幸的是,链接这两个实例会在它们之间创建一个强引用循环。 Person
实例现在有一个对 Apartment
实例的强引用,而 Apartment
实例有一个对 Person
实例的强引用。因此,当你打破 john
和 unit4A
变量持有的强引用时,引用计数不会降到零,ARC 不会释放这些实例:
john = nil
unit4A = nil
请注意,当你将这两个变量设置为 nil
时,它们的析构函数并未被调用。强烈的引用循环阻止了 Person
和 Apartment
实例的任何一次被释放,从而导致你的应用出现内存泄漏。
设置 john
和 unit4A
变量为 nil
后,强烈的引用关系如下所示:
Person
实例和 Apartment
实例之间的强烈引用仍然存在且无法被打破。
解决类实例之间的强烈引用循环
Swift 提供了两种解决类类型属性的强引用循环的方法:弱引用和未拥有引用。
弱引用和未拥有引用使得循环引用中的一个实例能够引用另一个实例而不保持强引用。这样实例之间就可以互相引用而不创建强引用循环。
当另一个实例有较短的生命周期时,即另一个实例可以先被释放时,使用弱引用。例如,在上面的 Apartment
示例中,公寓在某些时候可以没有租户,因此弱引用是打破这种引用循环的合适方式。相反,当另一个实例有相同的生命周期或更长的生命周期时,使用未拥有引用。
弱引用
弱引用不会对它所引用的对象保持强引用,因此不会阻止 ARC 释放被引用的对象。这种行为可以防止引用成为强引用循环的一部分。您可以通过在属性或变量声明前放置 weak
关键字来表示弱引用。
由于弱引用不会对它所引用的对象保持强引用,因此在弱引用仍然引用该对象时,该对象可能会被释放。因此,当弱引用所引用的对象被释放时,ARC 会自动将弱引用设置为 nil
。由于弱引用需要在运行时允许其值更改为 nil
,因此它们总是声明为可选类型的变量,而不是常量。
您可以像检查其他可选值一样检查弱引用中是否存在值,并且永远不会得到一个不再存在的无效实例的引用。
注意
属性观察者在 ARC 将弱引用设置为
nil
时不会被调用。
以下示例与上面的 Person
和 Apartment
示例相同,唯一的区别在于这次 Apartment
类的 tenant
属性被声明为弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
与两个变量( john
和 unit4A
)之间的强引用以及两个实例之间的链接相比,这次创建的方式相同:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
这是您将两个实例连接在一起后引用的外观:
Person
实例仍然对 Apartment
实例有强引用,但 Apartment
实例现在对 Person
实例有弱引用。这意味着当您通过将 john
变量设置为 nil
来断开 john
变量持有的强引用时, Person
实例将不再有任何强引用:
john = nil
// Prints "John Appleseed is being deinitialized"
由于不再有对 Person
实例的强引用,因此它被释放,并且 tenant
属性被设置为 nil
:
唯一剩余对 Apartment
实例的强引用来自 unit4A
变量。如果你断开这个强引用,就没有对 Apartment
实例的强引用了:
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
由于不再有对 Apartment
实例的强引用,因此它也被释放:
注意
在使用垃圾回收的系统中,有时会使用弱指针来实现简单的缓存机制,因为没有强引用的对象只有在内存压力触发垃圾回收时才会被释放。然而,在 ARC 中,一旦最后一个强引用被移除,值就会立即被释放,使得弱引用不适合用于这种目的。
未拥有引用
类似于弱引用,未拥有引用不会对它所引用的实例保持强引用。然而,与弱引用不同的是,未拥有引用用于表示其他实例具有相同的生命周期或更长的生命周期。您通过在属性或变量声明前放置 unowned
关键字来表示未拥有引用。
与弱引用不同,未拥有引用总是期望有一个值。因此,将一个值标记为未拥有并不会使其成为可选的,ARC 也不会将未拥有引用的值设置为 nil
。
重要
只有在您确定引用始终指向一个未被释放的实例时,才使用未拥有引用。
如果尝试在实例被释放后访问未拥有的引用的值,将会得到一个运行时错误。
以下示例定义了两个类 Customer
和 CreditCard
,它们分别表示银行客户和该客户的可能的信用卡。这两个类中的每一个都存储了另一个类的一个实例作为属性。这种关系有可能创建一个强引用循环。
Customer
和 CreditCard
之间的关系与上面弱引用示例中 Apartment
和 Person
之间的关系略有不同。在这个数据模型中,客户可能没有信用卡,但信用卡总是与客户相关联。 CreditCard
实例永远不会比它所引用的 Customer
实例存在更长时间。为了表示这一点, Customer
类有一个可选的 card
属性,但 CreditCard
类有一个未拥有的(且非可选的) customer
属性。
此外,新的 CreditCard
实例只能通过向自定义 CreditCard
初始化器传递一个 number
值和一个 customer
实例来创建。这确保了当创建 CreditCard
实例时, CreditCard
实例总是与一个 customer
实例相关联。
由于信用卡总是有一个客户,你将其 customer
属性定义为未拥有引用,以避免强引用循环:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
注意
number
类的CreditCard
属性定义为UInt64
类型而不是Int
类型,以确保number
属性的容量足够存储 16 位信用卡号码,无论是在 32 位还是 64 位系统上。
下一个代码片段定义了一个可选的 Customer
变量 john
,用于存储特定客户的引用。该变量的初始值为 nil
,因为它是一个可选变量:
var john: Customer?
现在你可以创建一个 Customer
实例,并使用它来初始化并分配一个新的 CreditCard
实例作为该客户的 card
属性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
现在你已经将两个实例链接起来了,引用看起来是这样的:
Customer
实例现在有一个强引用指向 CreditCard
实例,而 CreditCard
实例有一个弱引用指向 Customer
实例。
由于存在弱引用 customer
,当你打破 john
变量持有的强引用时, Customer
实例将不再有任何强引用:
既然 Customer
实例不再有任何强引用,它就会被释放。在此之后, CreditCard
实例也没有任何强引用,它也会被释放:
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上面的最终代码片段显示, Customer
实例和 CreditCard
实例的析构函数都在 john
变量被设置为 nil
之后打印它们的“析构”消息。
注意
上面的示例展示了如何使用安全未拥有引用。Swift 还提供了不安全未拥有引用,用于需要禁用运行时安全检查的情况——例如,出于性能原因。与所有不安全操作一样,您需要负责检查代码的安全性。
您通过写
unowned(unsafe)
来表示一个不安全未拥有引用。如果您尝试在引用它所指向的实例被释放后访问该不安全未拥有引用,程序将尝试访问实例曾经所在的内存位置,这是一项不安全的操作。
不拥有可选引用
您可以将可选的类引用标记为未拥有。在 ARC 所有权模型中,未拥有可选引用和弱引用可以在相同的上下文中使用。不同之处在于,当您使用未拥有可选引用时,您需要确保它始终指向一个有效的对象或设置为 nil
。
这是一个跟踪学校中某个系提供的课程的示例:
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
Department
为每个系提供的课程保持强引用。在 ARC 所有权模型中,系拥有其课程。 Course
有两个未拥有引用,一个指向系,一个指向学生应选的下一门课程;课程不拥有这两个对象。每个课程都是某个系的一部分,因此 department
属性不是可选的。然而,因为有些课程没有推荐的后续课程,所以 nextCourse
属性是可选的。
这是使用这些类的示例:
let department = Department(name: "Horticulture")
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
上述代码创建了一个部门及其三个课程。入门课程和中级课程都存储了在它们的 nextCourse
属性中的建议下一门课程,该属性维护了一个对学生完成这门课程后应选修的课程的非所有者可选引用。
未拥有的可选引用不会对它所包装的类的实例保持强引用,因此不会阻止 ARC 释放该实例。它在 ARC 下的行为与未拥有的引用相同,只是可选的未拥有的引用可以是 nil
。
与非可选的未拥有的引用类似,您需要确保 nextCourse
始终指向一个未被释放的课程。例如,在从 department.courses
中删除课程时,您还需要移除其他课程可能拥有的对该课程的任何引用。
注意
可选值的基础类型是
Optional
,这是 Swift 标准库中的一个枚举。然而,可选值是值类型不能标记为unowned
规则的例外。包装类的可选值不使用引用计数,因此您不需要保持对可选值的强引用。
未拥有引用和隐式未包装的可选属性
上面关于弱引用和未拥有引用的示例涵盖了需要打破强引用循环的两种更常见的情况。
Person
和 Apartment
示例展示了两个都允许为 nil
的属性可能造成强引用循环的情况。这种场景最好使用弱引用来解决。
Customer
和 CreditCard
示例展示了其中一个允许为 nil
的属性和另一个不允许为 nil
的属性可能造成强引用循环的情况。这种场景最好使用未拥有引用来解决。
然而,还有一种情况,即两个属性在初始化完成后始终应该有值,且两个属性都不应该初始化完成后为 nil
。在这种情况下,可以在一个类中使用非拥有的属性,在另一个类中使用隐式解包的可选属性。
这样,在初始化完成后,可以直接访问这两个属性(无需解包),同时仍然避免了引用循环。本节将向您展示如何设置这种关系。
以下示例定义了两个类 Country
和 City
,每个类都将其他类的一个实例作为属性存储。在这个数据模型中,每个国家必须始终有一个首都,每个城市也必须始终属于一个国家。为了表示这一点, Country
类有一个 capitalCity
属性,而 City
类有一个 country
属性:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
为了在两个类之间设置这种依赖关系, City
类的初始化器接受一个 Country
实例,并将其存储在其 country
属性中。
City
的初始化器在 Country
的初始化器内部被调用。然而, Country
的初始化器在 self
完全初始化之前不能将 self
传递给 City
的初始化器,如两阶段初始化中所述。
为了满足这一要求,你将 capitalCity
属性声明为隐式解包的可选属性,通过在其类型注解末尾添加感叹号( Country
)来表示。这意味着 capitalCity
属性具有默认值 nil
,就像其他可选属性一样,但可以在不需要解包其值的情况下访问它,如隐式解包的可选属性中所述。
由于 capitalCity
具有默认 nil
值,因此只要 Country
实例在其初始化器中设置其 Country
属性,Country
实例就被认为已经完全初始化。这意味着 Country
的初始化器可以一但 name
属性被设置就可以开始引用和传递隐式的 self
属性。因此, Country
的初始化器可以在设置其自己的 capitalCity
属性时将 self
作为 City
初始化器的一个参数传递给 Country
初始化器。
这意味着你可以在一个语句中创建 Country
和 City
实例,而不会创建强引用循环,并且可以直接访问 capitalCity
属性,而不需要使用感叹号来解包其可选值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
在上面的示例中,使用隐式解包的可选值意味着满足了两个阶段类初始化的所有要求。一旦初始化完成, capitalCity
属性可以像非可选值一样使用和访问,同时仍然避免了强引用循环。
强引用循环与闭包
你上面看到了当两个类实例属性彼此持有强引用时如何创建强引用循环。你还看到了如何使用弱引用和未拥有引用来打破这些强引用循环。
如果你将一个闭包赋值给类实例的一个属性,并且闭包的主体捕获了该实例,也可能发生强引用循环。这种捕获可能是因为闭包的主体访问了实例的属性,例如 self.someProperty
,或者是因为闭包调用了实例的方法,例如 self.someMethod()
。无论哪种情况,这些访问都会使闭包“捕获” self
,从而创建一个强引用循环。
这个强引用循环是因为闭包像类一样,是一种引用类型。当你将一个闭包赋值给一个属性时,你实际上是在赋值该闭包的引用。本质上,这与上述问题相同——两个强引用互相保持对方存活。不过,这次不是两个类实例,而是类实例和一个闭包互相保持对方存活。
Swift 为解决这个问题提供了一个优雅的解决方案,称为闭包捕获列表。然而,在学习如何使用闭包捕获列表来打破强引用循环之前,了解这种循环是如何产生的很有用。
以下示例展示了如何在使用引用 self
的闭包时创建强引用循环。该示例定义了一个名为 HTMLElement
的类,该类为 HTML 文档中的单个元素提供了一个简单的模型:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElement
类定义了一个 name
属性,该属性表示元素的名称,例如对于标题元素使用 "h1"
,对于段落元素使用 "p"
,对于换行元素使用 "br"
。 HTMLElement
类还定义了一个可选的 text
属性,您可以将其设置为表示要在该 HTML 元素内呈现的文本的字符串。
除了这两个简单的属性之外, HTMLElement
类还定义了一个名为 asHTML
的懒属性。该属性引用了一个闭包,该闭包将 name
和 text
结合成一个 HTML 字符串片段。 asHTML
属性的类型为 () -> String
,即“不带参数的函数,返回一个 String
值”。
默认情况下, asHTML
属性被分配了一个闭包,该闭包返回一个 HTML 标签的字符串表示。如果存在 text
值,则包含该值;如果不存在 text
值,则不包含任何文本内容。对于段落元素,闭包将返回 "<p>some text</p>"
或 "<p />"
,具体取决于 text
属性等于 "some text"
还是 nil
。
asHTML
属性被命名并像实例方法一样使用。然而,由于 asHTML
是一个闭包属性而不是实例方法,因此您可以将 asHTML
属性的默认值替换为自定义闭包,以便更改特定 HTML 元素的 HTML 渲染。
例如, asHTML
属性可以设置为一个默认返回某些文本的闭包,如果 text
属性是 nil
,以防止表示返回空的 HTML 标签。
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
注意
asHTML
属性声明为一个懒加载属性,因为它只有在实际需要将元素渲染为字符串值以供某些 HTML 输出目标使用时才需要。由于asHTML
是一个懒加载属性,因此您可以在默认闭包中引用self
,因为懒加载属性将在初始化完成后才被访问,并且此时self
已经存在。
HTMLElement
类提供了一个单一的初始化器,该初始化器接受一个 name
参数(如果需要的话),并初始化一个新的元素。该类还定义了一个析构函数,该析构函数打印一条消息以显示何时销毁一个 HTMLElement
实例。
这是如何使用 HTMLElement
类创建并打印一个新实例的方法:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
注意
上面的
paragraph
变量被定义为可选的HTMLElement
,因此可以在下面将其设置为nil
以演示强引用循环的存在。
不幸的是,上面编写的 HTMLElement
类在默认的 asHTML
值中使用闭包时,会在 HTMLElement
实例和其闭包之间创建一个强引用循环。循环如下所示:
实例的 asHTML
属性持有对其闭包的强引用。然而,由于闭包在其主体中引用了 self
(作为引用 self.name
和 self.text
的一种方式),闭包捕获了 self
,这意味着它也持有对 HTMLElement
实例的强引用。因此在两者之间创建了一个强引用循环。(有关闭包中捕获值的更多信息,请参阅捕获值。)
注意
尽管闭包多次引用了
self
,但它只捕获了一个对HTMLElement
实例的强引用。
如果你将 paragraph
变量设置为 nil
并断开其与 HTMLElement
实例的强引用,强引用循环会阻止释放 HTMLElement
实例及其闭包:
paragraph = nil
注意 HTMLElement
去初始化器中的消息没有被打印,这表明 HTMLElement
实例没有被释放。
解决闭包中的强引用循环
通过在闭包定义中定义捕获列表来解决闭包与类实例之间的强引用循环。捕获列表定义了在闭包主体中捕获一个或多个引用类型时使用的规则。就像两个类实例之间的强引用循环一样,您需要将每个捕获的引用声明为弱引用或非所有者引用,而不是强引用。弱引用或非所有者引用的选择取决于代码不同部分之间的关系。
注意
每当您在闭包中引用
self
的成员时,Swift 要求您写self.someProperty
或self.someMethod()
(而不是someProperty
或someMethod()
)。这有助于您记住有可能意外捕获self
。
定义捕获列表
捕获列表中的每个项都是 weak
或 unowned
关键字与其对某个类实例(如 self
)或已初始化为某个值的变量(如 delegate = self.delegate
)的引用的配对。这些配对写在一对方括号内,用逗号分隔。
在闭包的参数列表和返回类型(如果提供的话)之前放置捕获列表:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
如果闭包没有指定参数列表或返回类型,因为可以从上下文中推断出来,则将捕获列表放在闭包的最前面,紧跟 in
关键字:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] 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)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
这个 HTMLElement
的实现与之前的实现相同,除了在 asHTML
闭包中添加了一个捕获列表。在这种情况下,捕获列表是 [unowned self]
,这意味着“捕获 self
为一个非所有者引用而不是一个强引用”。
你可以像之前那样创建并打印一个 HTMLElement
实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
这里是捕获列表生效后的引用情况:
这次,闭包对 self
的捕获是一个非所有者引用,并不会对 HTMLElement
实例保持强引用。如果你将 paragraph
变量对 nil
的强引用设置为 HTMLElement
, HTMLElement
实例将被释放,如下例中的销毁消息所示:
paragraph = nil
// Prints "p is being deinitialized"
对于捕获列表的更多信息,请参见捕获列表。