Swift解決實例之間的循環強引用
解決實例之間的循環強引用
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
屬性在初始化完成後,能像非可選值一樣使用和存取同時還避免了循環強引用。