解决实例之间的循环强引用
Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。
弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。
对于生命周期中会变为nil
的实例使用弱引用。相反的,对于初始化赋值后再也不会被赋值为nil
的实例,使用无主引用。
弱引用
弱引用不会牢牢保持住引用的实例,并且不会阻止 ARC 销毁被引用的实例。这种行为阻止了引用变为循环强引用。声明属性或者变量时,在前面加上weak
关键字表明这是一个弱引用。
在实例的生命周期中,如果某些时候引用没有值,那么弱引用可以阻止循环强引用。如果引用总是有值,则可以使用无主引用,在无主引用中有描述。在上面Apartment
的例子中,一个公寓的生命周期中,有时是没有“居民”的,因此适合使用弱引用来解决循环强引用。
注意:
弱引用必须被声明为变量,表明其值能在运行时被修改。弱引用不能被声明为常量。
因为弱引用可以没有值,你必须将每一个弱引用声明为可选类型。可选类型是在 Swift 语言中推荐的用来表示可能没有值的类型。
因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其赋值为nil
。你可以像其他可选值一样,检查弱引用的值是否存在,你永远也不会遇到被销毁了而不存在的实例。
下面的例子跟上面Person
和Apartment
的例子一致,但是有一个重要的区别。这一次,Apartment
的tenant
属性被声明为弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
weak var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}
然后跟之前一样,建立两个变量(john和number73)之间的强引用,并关联两个实例:
var john: Person?
var number73: Apartment?
john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)
john!.apartment = number73
number73!.tenant = john
现在,两个关联在一起的实例的引用关系如下图所示:
Person
实例依然保持对Apartment
实例的强引用,但是Apartment
实例只是对Person
实例的弱引用。这意味着当你断开john
变量所保持的强引用时,再也没有指向Person
实例的强引用了:
由于再也没有指向Person
实例的强引用,该实例会被销毁:
john = nil
// prints "John Appleseed is being deinitialized"
唯一剩下的指向Apartment
实例的强引用来自于变量number73
。如果你断开这个强引用,再也没有指向Apartment
实例的强引用了:
由于再也没有指向Apartment
实例的强引用,该实例也会被销毁:
number73 = nil
// prints "Apartment #73 is being deinitialized"
上面的两段代码展示了变量john
和number73
在被赋值为nil
后,Person
实例和Apartment
实例的析构函数都打印出“销毁”的信息。这证明了引用循环被打破了。
无主引用
和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型(non-optional type)。你可以在声明属性或者变量时,在前面加上关键字unowned
表示这是一个无主引用。
由于无主引用是非可选类型,你不需要在使用它的时候将它展开。无主引用总是可以被直接访问。不过 ARC 无法在实例被销毁后将无主引用设为nil
,因为非可选类型的变量不允许被赋值为nil
。
注意:
如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。使用无主引用,你必须确保引用始终指向一个未销毁的实例。
还需要注意的是如果你试图访问实例已经被销毁的无主引用,程序会直接崩溃,而不会发生无法预期的行为。所以你应当避免这样的事情发生。
下面的例子定义了两个类,Customer
和CreditCard
,模拟了银行客户和客户的信用卡。这两个类中,每一个都将另外一个类的实例作为自身的属性。这种关系会潜在的创造循环强引用。
Customer
和CreditCard
之间的关系与前面弱引用例子中Apartment
和Person
的关系截然不同。在这个数据模型中,一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系,Customer
类有一个可选类型的card
属性,但是CreditCard
类有一个非可选类型的customer
属性。
此外,只能通过将一个number
值和customer
实例传递给CreditCard
构造函数的方式来创建CreditCard
实例。这样可以确保当创建CreditCard
实例时总是有一个customer
实例与之关联。
由于信用卡总是关联着一个客户,因此将customer
属性定义为无主引用,用以避免循环强引用:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { println("\(name) is being deinitialized") }
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized") }
}
下面的代码片段定义了一个叫john
的可选类型Customer
变量,用来保存某个特定客户的引用。由于是可选类型,所以变量被初始化为nil
。
var john: Customer?
现在你可以创建Customer
类的实例,用它初始化CreditCard
实例,并将新创建的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"
最后的代码展示了在john
变量被设为nil
后Customer
实例和CreditCard
实例的构造函数都打印出了“销毁”的信息。
无主引用以及隐式解析可选属性
上面弱引用和无主引用的例子涵盖了两种常用的需要打破循环强引用的场景。
Person
和Apartment
的例子展示了两个属性的值都允许为nil
,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。
Customer
和CreditCard
的例子展示了一个属性的值允许为nil
,而另一个属性的值不允许为nil
,并会潜在的产生循环强引用。这种场景最适合通过无主引用来解决。
然而,存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完成后不能为nil
。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。
这使两个属性在初始化完成后能被直接访问(不需要可选展开),同时避免了循环引用。这一节将为你展示如何建立这种关系。
下面的例子定义了两个类,Country
和City
,每个类将另外一个类的实例保存为属性。在这个模型中,每个国家必须有首都,而每一个城市必须属于一个国家。为了实现这种关系,Country
类拥有一个capitalCity
属性,而City
类有一个country
属性:
class Country {
let name: String
let 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
属性。
Country
的构造函数调用了City
的构造函数。然而,只有Country
的实例完全初始化完后,Country
的构造函数才能把self
传给City
的构造函数。(在两段式构造过程中有具体描述)
为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将Country
的capitalCity
属性声明为隐式解析可选类型的属性。这表示像其他可选类型一样,capitalCity
属性的默认值为nil
,但是不需要展开它的值就能访问它。(在隐式解析可选类型中有描述)
由于capitalCity
默认值为nil
,一旦Country
的实例在构造函数中给name
属性赋值后,整个初始化过程就完成了。这代表一旦name
属性被赋值后,Country
的构造函数就能引用并传递隐式的self
。Country
的构造函数在赋值capitalCity
时,就能将self
作为参数传递给City
的构造函数。
以上的意义在于你可以通过一条语句同时创建Country
和City
的实例,而不产生循环强引用,并且capitalCity
的属性能被直接访问,而不需要通过感叹号来展开它的可选值:
var country = Country(name: "Canada", capitalName: "Ottawa")
println("\(country.name)'s capital city is called \(country.capitalCity.name)")
// prints "Canada's capital city is called Ottawa"
在上面的例子中,使用隐式解析可选值的意义在于满足了两个类构造函数的需求。capitalCity
属性在初始化完成后,能像非可选值一样使用和存取同时还避免了循环强引用。
易百教程移动端:请扫描本页面底部(右侧)二维码并关注微信公众号,回复:"教程" 选择相关教程阅读或直接访问:http://m.yiibai.com 。
加QQ群啦,易百教程官方技术学习群
注意:建议每个人选自己的技术方向加群,同一个QQ最多限加 3 个群。
- Java技术群: 227270512 (人数:2000,免费:否)
- Go开发者群(新): 851549018 (人数:1000,免费)
- PHP开发者群: 460153241 (人数:2000,免费)
- MySQL/SQL群: 418407075 (人数:2000,免费:否)
- 大数据开发群: 655154550 (人数:2000,免费:否)
- Python技术群: 287904175 (人数:2000,免费:否)
- 人工智能深度学习: 456236082 (人数:2000,免费:否)
- 测试工程师群: 415553199 (人数:2000,免费:否)
- 前端开发者群: 410430016 (人数:2000,免费:否)
- C/C++技术群(新): 629264796 (人数:2000,免费)
- Node.js技术群(新): 621549808 (人数:2000,免费)
- PostgreSQL数据库群: 539504187 (人数:1000,免费)
- Linux运维技术群: 479429477 (人数:2000,免费:否)
- Oracle数据库: 175248146 (人数:2000,免费:否)
- C#/ASP.Net开发者: 579821706 (人数:2000,免费)
- 数据分析师群: 397883996 (人数:2000,免费:否)