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