位置:首頁 > 高級語言 > Swift教學 > Swift解決實例之間的循環強引用

Swift解決實例之間的循環強引用

解決實例之間的循環強引用

Swift 提供了兩種辦法用來解決你在使用類的屬性時所遇到的循環強引用問題:弱引用(weak reference)和無主引用(unowned reference)。

弱引用和無主引用允許循環引用中的一個實例引用另外一個實例而不保持強引用。這樣實例能夠互相引用而不產生循環強引用。

對於生命周期中會變為nil的實例使用弱引用。相反的,對於初始化賦值後再也不會被賦值為nil的實例,使用無主引用。

弱引用

弱引用不會牢牢保持住引用的實例,並且不會阻止 ARC 銷毀被引用的實例。這種行為阻止了引用變為循環強引用。聲明屬性或者變量時,在前麵加上weak關鍵字表明這是一個弱引用。

在實例的生命周期中,如果某些時候引用冇有值,那麼弱引用可以阻止循環強引用。如果引用總是有值,則可以使用無主引用,在無主引用中有描述。在上麵Apartment的例子中,一個公寓的生命周期中,有時是冇有“居民”的,因此適合使用弱引用來解決循環強引用。


注意:
弱引用必須被聲明為變量,表明其值能在運行時被修改。弱引用不能被聲明為常量。
 

因為弱引用可以冇有值,你必須將每一個弱引用聲明為可選類型。可選類型是在 Swift 語言中推薦的用來表示可能冇有值的類型。

因為弱引用不會保持所引用的實例,即使引用存在,實例也有可能被銷毀。因此,ARC 會在引用的實例被銷毀後自動將其賦值為nil。你可以像其他可選值一樣,檢查弱引用的值是否存在,你永遠也不會遇到被銷毀了而不存在的實例。

下麵的例子跟上麵PersonApartment的例子一致,但是有一個重要的區彆。這一次,Apartmenttenant屬性被聲明為弱引用:

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"

上麵的兩段代碼展示了變量johnnumber73在被賦值為nil後,Person實例和Apartment實例的析構函數都打印出“銷毀”的信息。這證明了引用循環被打破了。

無主引用

和弱引用類似,無主引用不會牢牢保持住引用的實例。和弱引用不同的是,無主引用是永遠有值的。因此,無主引用總是被定義為非可選類型(non-optional type)。你可以在聲明屬性或者變量時,在前麵加上關鍵字unowned表示這是一個無主引用。

由於無主引用是非可選類型,你不需要在使用它的時候將它展開。無主引用總是可以被直接訪問。不過 ARC 無法在實例被銷毀後將無主引用設為nil,因為非可選類型的變量不允許被賦值為nil


注意:
如果你試圖在實例被銷毀後,訪問該實例的無主引用,會觸發運行時錯誤。使用無主引用,你必須確保引用始終指向一個未銷毀的實例。
還需要注意的是如果你試圖訪問實例已經被銷毀的無主引用,程序會直接崩潰,而不會發生無法預期的行為。所以你應當避免這樣的事情發生。
 

下麵的例子定義了兩個類,CustomerCreditCard,模擬了銀行客戶和客戶的信用卡。這兩個類中,每一個都將另外一個類的實例作為自身的屬性。這種關係會潛在的創造循環強引用。

CustomerCreditCard之間的關係與前麵弱引用例子中ApartmentPerson的關係截然不同。在這個數據模型中,一個客戶可能有或者冇有信用卡,但是一張信用卡總是關聯著一個客戶。為了表示這種關係,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變量被設為nilCustomer實例和CreditCard實例的構造函數都打印出了“銷毀”的信息。

無主引用以及隱式解析可選屬性

上麵弱引用和無主引用的例子涵蓋了兩種常用的需要打破循環強引用的場景。

PersonApartment的例子展示了兩個屬性的值都允許為nil,並會潛在的產生循環強引用。這種場景最適合用弱引用來解決。

CustomerCreditCard的例子展示了一個屬性的值允許為nil,而另一個屬性的值不允許為nil,並會潛在的產生循環強引用。這種場景最適合通過無主引用來解決。

然而,存在著第三種場景,在這種場景中,兩個屬性都必須有值,並且初始化完成後不能為nil。在這種場景中,需要一個類使用無主屬性,而另外一個類使用隱式解析可選屬性。

這使兩個屬性在初始化完成後能被直接訪問(不需要可選展開),同時避免了循環引用。這一節將為你展示如何建立這種關係。

下麵的例子定義了兩個類,CountryCity,每個類將另外一個類的實例保存為屬性。在這個模型中,每個國家必須有首都,而每一個城市必須屬於一個國家。為了實現這種關係,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!)的方式,將CountrycapitalCity屬性聲明為隱式解析可選類型的屬性。這表示像其他可選類型一樣,capitalCity屬性的默認值為nil,但是不需要展開它的值就能訪問它。(在隱式解析可選類型中有描述)

由於capitalCity默認值為nil,一旦Country的實例在構造函數中給name屬性賦值後,整個初始化過程就完成了。這代表一旦name屬性被賦值後,Country的構造函數就能引用並傳遞隱式的selfCountry的構造函數在賦值capitalCity時,就能將self作為參數傳遞給City的構造函數。

以上的意義在於你可以通過一條語句同時創建CountryCity的實例,而不產生循環強引用,並且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屬性在初始化完成後,能像非可選值一樣使用和存取同時還避免了循環強引用。