《The Swift Programming Language》2.0版之自動引用計數

Swift 1.0文檔翻譯:TimothyYe
Swift 1.0文檔校對:Hawstein
Swift 2.0文檔校對及翻譯潤色:Channegit

PS:以前1.0版中文版看不懂地方在對比英文版後就懂了,仍是以前翻譯的不夠準確啊。此次參與Swift 2.0文檔ARC章節的校對翻譯,順便潤色一下部分翻譯,以便你們更好的理解原文的意思。github

# 自動引用計數

本頁包含內容:swift

Swift 使用自動引用計數(ARC)機制來跟蹤和管理你的應用程序的內存。一般狀況下,Swift 的內存管理機制會一直起着做用,你無須本身來考慮內存的管理。ARC 會在類的實例再也不被使用時,自動釋放其佔用的內存。api

然而,在少數狀況下,ARC 爲了能幫助你管理內存,須要更多的關於你的代碼之間關係的信息。本章描述了這些狀況,而且爲你示範怎樣啓用 ARC 來管理你的應用程序的內存。閉包

注意:
引用計數僅僅應用於類的實例。結構體和枚舉類型是值類型,不是引用類型,也不是經過引用的方式存儲和傳遞。函數

自動引用計數的工做機制

當你每次建立一個類的新的實例的時候,ARC 會分配一大塊內存用來儲存實例的信息。內存中會包含實例的類型信息,以及這個實例全部相關屬性的值。性能

此外,當實例再也不被使用時,ARC 釋放實例所佔用的內存,並讓釋放的內存能挪做他用。這確保了再也不被使用的實例,不會一直佔用內存空間。學習

然而,當 ARC 收回和釋放了正在被使用中的實例,該實例的屬性和方法將不能再被訪問和調用。實際上,若是你試圖訪問這個實例,你的應用程序極可能會崩潰。spa

爲了確保使用中的實例不會被銷燬,ARC 會跟蹤和計算每個實例正在被多少屬性,常量和變量所引用。哪怕實例的引用數爲1,ARC都不會銷燬這個實例。翻譯

爲了使上述成爲可能,不管你將實例賦值給屬性、常量或變量,它們都會建立此實例的強引用。之因此稱之爲「強」引用,是由於它會將實例緊緊的保持住,只要強引用還在,實例是不容許被銷燬的。

自動引用計數實踐

下面的例子展現了自動引用計數的工做機制。例子以一個簡單的Person類開始,並定義了一個叫name的常量屬性:

swiftclass 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類的實例。

swiftvar reference1: Person?
var reference2: Person?
var reference3: Person?

如今你能夠建立Person類的新實例,而且將它賦值給三個變量中的一個:

swiftreference1 = Person(name: "John Appleseed")
// prints "John Appleseed is being initialized」

應當注意到當你調用Person類的構造函數的時候,"John Appleseed is being initialized」會被打印出來。由此能夠肯定構造函數被執行。

因爲Person類的新實例被賦值給了reference1變量,因此reference1Person類的新實例之間創建了一個強引用。正是由於這一個強引用,ARC 會保證Person實例被保持在內存中不被銷燬。

若是你將同一個Person實例也賦值給其餘兩個變量,該實例又會多出兩個強引用:

swiftreference2 = reference1
reference3 = reference1

如今這一個Person實例已經有三個強引用了。

若是你經過給其中兩個變量賦值nil的方式斷開兩個強引用(包括最早的那個強引用),只留下一個強引用,Person實例不會被銷燬:

swiftreference1 = nil
reference2 = nil

在你清楚地代表再也不使用這個Person實例時,即第三個也就是最後一個強引用被斷開時,ARC 會銷燬它。

swiftreference3 = nil
// prints "John Appleseed is being deinitialized"

類實例之間的循環強引用

在上面的例子中,ARC 會跟蹤你所新建立的Person實例的引用數量,而且會在Person實例再也不被須要時銷燬它。

然而,咱們可能會寫出一個類實例的強引用數永遠不能變成0的代碼。若是兩個類實例互相持有對方的強引用,於是每一個實例都讓對方一直存在,就是這種狀況。這就是所謂的循環強引用。

你能夠經過定義類之間的關係爲弱引用或無主引用,以替代強引用,從而解決循環強引用的問題。具體的過程在解決類實例之間的循環強引用中有描述。無論怎樣,在你學習怎樣解決循環強引用以前,頗有必要了解一下它是怎樣產生的。

下面展現了一個不經意產生循環強引用的例子。例子定義了兩個類:PersonApartment,用來建模公寓和它其中的居民:

swiftclass Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}
swiftclass Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    var tenant: Person?
    deinit { print("Apartment #\(number) is being deinitialized") }
}

每個Person實例有一個類型爲String,名字爲name的屬性,並有一個可選的初始化爲nilapartment屬性。apartment屬性是可選的,由於一我的並不老是擁有公寓。

相似的,每一個Apartment實例有一個叫number,類型爲Int的屬性,並有一個可選的初始化爲niltenant屬性。tenant屬性是可選的,由於一棟公寓並不老是有居民。

這兩個類都定義了析構函數,用以在類實例被析構的時候輸出信息。這讓你可以知曉PersonApartment的實例是否像預期的那樣被銷燬。

接下來的代碼片斷定義了兩個可選類型的變量johnnumber73,並分別被設定爲下面的ApartmentPerson的實例。這兩個變量都被初始化爲nil,這正是可選的優勢:

swiftvar john: Person?
var number73: Apartment?

如今你能夠建立特定的PersonApartment實例並將賦值給johnnumber73變量:

swiftjohn = Person(name: "John Appleseed")
number73 = Apartment(number: 73)

在兩個實例被建立和賦值後,下圖表現了強引用的關係。變量john如今有一個指向Person實例的強引用,而變量number73有一個指向Apartment實例的強引用:

如今你可以將這兩個實例關聯在一塊兒,這樣人就能有公寓住了,而公寓也有了房客。注意感嘆號是用來展開和訪問可選變量johnnumber73中的實例,這樣實例的屬性才能被賦值:

swiftjohn!.apartment = number73
number73!.tenant = john

在將兩個實例聯繫在一塊兒以後,強引用的關係如圖所示:

不幸的是,這兩個實例關聯後會產生一個循環強引用。Person實例如今有了一個指向Apartment實例的強引用,而Apartment實例也有了一個指向Person實例的強引用。所以,當你斷開johnnumber73變量所持有的強引用時,引用計數並不會降爲 0,實例也不會被 ARC 銷燬:

swiftjohn = nil
number73 = nil

注意,當你把這兩個變量設爲nil時,沒有任何一個析構函數被調用。循環強引用會一直阻止PersonApartment類實例的銷燬,這就在你的應用程序中形成了內存泄漏。

在你將johnnumber73賦值爲nil後,強引用關係以下圖:

PersonApartment實例之間的強引用關係保留了下來而且不會被斷開。

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

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

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

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

弱引用

弱引用不會對其引用的實例保持強引用,於是不會阻止 ARC 銷燬被引用的實例。這個特性阻止了引用變爲循環強引用。聲明屬性或者變量時,在前面加上weak關鍵字代表這是一個弱引用。

在實例的生命週期中,若是某些時候引用沒有值,那麼弱引用能夠避免循環強引用。若是引用老是有值,則可使用無主引用,在無主引用中有描述。在上面Apartment的例子中,一個公寓的生命週期中,有時是沒有「居民」的,所以適合使用弱引用來解決循環強引用。

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

由於弱引用能夠沒有值,你必須將每個弱引用聲明爲可選類型。在 Swift 中,推薦使用可選類型描述可能沒有值的類型。

由於弱引用不會保持所引用的實例,即便引用存在,實例也有可能被銷燬。所以,ARC 會在引用的實例被銷燬後自動將其賦值爲nil。你能夠像其餘可選值同樣,檢查弱引用的值是否存在,你將永遠不會訪問已銷燬的實例的引用。

下面的例子跟上面PersonApartment的例子一致,可是有一個重要的區別。這一次,Apartmenttenant屬性被聲明爲弱引用:

swiftclass Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}
swiftclass Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    weak var tenant: Person?
    deinit { print("Apartment #\(number) is being deinitialized") }
}

而後跟以前同樣,創建兩個變量(johnnumber73)之間的強引用,並關聯兩個實例:

swiftvar 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實例的強引用,該實例會被銷燬:

swiftjohn = nil
// prints "John Appleseed is being deinitialized"

惟一剩下的指向Apartment實例的強引用來自於變量number73。若是你斷開這個強引用,再也沒有指向Apartment實例的強引用了:

因爲再也沒有指向Apartment實例的強引用,該實例也會被銷燬:

swiftnumber73 = nil
// prints "Apartment #73 is being deinitialized"

上面的兩段代碼展現了變量johnnumber73在被賦值爲nil後,Person實例和Apartment實例的析構函數都打印出「銷燬」的信息。這證實了引用循環被打破了。

無主引用

和弱引用相似,無主引用不會緊緊保持住引用的實例。和弱引用不一樣的是,無主引用是永遠有值的。所以,無主引用老是被定義爲非可選類型(non-optional type)。你能夠在聲明屬性或者變量時,在前面加上關鍵字unowned表示這是一個無主引用。

因爲無主引用是非可選類型,你不須要在使用它的時候將它展開。無主引用老是能夠被直接訪問。不過 ARC 沒法在實例被銷燬後將無主引用設爲nil,由於非可選類型的變量不容許被賦值爲nil

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

下面的例子定義了兩個類,CustomerCreditCard,模擬了銀行客戶和客戶的信用卡。這兩個類中,每個都將另一個類的實例做爲自身的屬性。這種關係可能會形成循環強引用。

CustomerCreditCard之間的關係與前面弱引用例子中ApartmentPerson的關係略微不一樣。在這個數據模型中,一個客戶可能有或者沒有信用卡,可是一張信用卡老是關聯着一個客戶。爲了表示這種關係,Customer類有一個可選類型的card屬性,可是CreditCard類有一個非可選類型的customer屬性。

此外,只能經過將一個number值和customer實例傳遞給CreditCard構造函數的方式來建立CreditCard實例。這樣能夠確保當建立CreditCard實例時老是有一個customer實例與之關聯。

因爲信用卡老是關聯着一個客戶,所以將customer屬性定義爲無主引用,用以免循環強引用:

swiftclass Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}
swiftclass 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") }
}

注意:
CreditCard類的number屬性被定義爲UInt64類型而不是Int類型,以確保number屬性的存儲量在32位和64位系統上都能足夠容納16位的卡號。

下面的代碼片斷定義了一個叫john的可選類型Customer變量,用來保存某個特定客戶的引用。因爲是可選類型,因此變量被初始化爲nil

swiftvar john: Customer?

如今你能夠建立Customer類的實例,用它初始化CreditCard實例,並將新建立的CreditCard實例賦值爲客戶的card屬性。

swiftjohn = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

在你關聯兩個實例後,它們的引用關係以下圖所示:

Customer實例持有對CreditCard實例的強引用,而CreditCard實例持有對Customer實例的無主引用。

因爲customer的無主引用,當你斷開john變量持有的強引用時,再也沒有指向Customer實例的強引用了:

因爲再也沒有指向Customer實例的強引用,該實例被銷燬了。其後,再也沒有指向CreditCard實例的強引用,該實例也隨之被銷燬了:

swiftjohn = 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屬性:

swiftclass Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}
swiftclass 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的屬性能被直接訪問,而不須要經過感嘆號來展開它的可選值:

swiftvar 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 提供了一種優雅的方法來解決這個問題,稱之爲閉包捕獲列表(closuer capture list)。一樣的,在學習如何用閉包捕獲列表破壞循環強引用以前,先來了解一下這裏的循環強引用是如何產生的,這對咱們頗有幫助。

下面的例子爲你展現了當一個閉包引用了self後是如何產生一個循環強引用的。例子中定義了一個叫HTMLElement的類,用一種簡單的模型表示 HTML 中的一個單獨的元素:

swiftclass HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: Void -> 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屬性來表示這個元素的名稱,例如表明段落的"p",或者表明換行的"br"。HTMLElement還定義了一個可選屬性text,用來設置和展示 HTML 元素的文本。

除了上面的兩個屬性,HTMLElement還定義了一個lazy屬性asHTML。這個屬性引用了一個將nametext組合成 HTML 字符串片斷的閉包。該屬性是Void -> String類型,或者能夠理解爲「一個沒有參數,返回String的函數」。

默認狀況下,閉包賦值給了asHTML屬性,這個閉包返回一個表明 HTML 標籤的字符串。若是text值存在,該標籤就包含可選值text;若是text不存在,該標籤就不包含文本。對於段落元素,根據text"some text"仍是nil,閉包會返回"<p>some text</p>"或者"<p />"。

能夠像實例方法那樣去命名、使用asHTML屬性。然而,因爲asHTML是閉包而不是實例方法,若是你想改變特定元素的 HTML 處理的話,能夠用自定義的閉包來取代默認值。

注意:
asHTML聲明爲lazy屬性,由於只有當元素確實須要處理爲HTML輸出的字符串時,才須要使用asHTML。也就是說,在默認的閉包中可使用self,由於只有當初始化完成以及self確實存在後,才能訪問lazy屬性。

HTMLElement類只提供一個構造函數,經過nametext(若是有的話)參數來初始化一個元素。該類也定義了一個析構函數,當HTMLElement實例被銷燬時,打印一條消息。

下面的代碼展現瞭如何用HTMLElement類建立實例並打印消息。

swiftvar paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints"hello, world"

注意:
上面的paragraph變量定義爲可選HTMLElement,所以咱們能夠賦值nil給它來演示循環強引用。

不幸的是,上面寫的HTMLElement類產生了類實例和asHTML默認值的閉包之間的循環強引用。循環強引用以下圖所示:

實例的asHTML屬性持有閉包的強引用。可是,閉包在其閉包體內使用了self(引用了self.nameself.text),所以閉包捕獲了self,這意味着閉包又反過來持有了HTMLElement實例的強引用。這樣兩個對象就產生了循環強引用。(更多關於閉包捕獲值的信息,請參考值捕獲)。

注意:
雖然閉包屢次使用了self,它只捕獲HTMLElement實例的一個強引用。

若是設置paragraph變量爲nil,打破它持有的HTMLElement實例的強引用,HTMLElement實例和它的閉包都不會被銷燬,也是由於循環強引用:

swiftparagraph = nil

注意HTMLElementdeinitializer中的消息並無被打印,證實了HTMLElement實例並無被銷燬。

解決閉包引發的循環強引用

在定義閉包時同時定義捕獲列表做爲閉包的一部分,經過這種方式能夠解決閉包和類實例之間的循環強引用。捕獲列表定義了閉包體內捕獲一個或者多個引用類型的規則。跟解決兩個類實例間的循環強引用同樣,聲明每一個捕獲的引用爲弱引用或無主引用,而不是強引用。應當根據代碼關係來決定使用弱引用仍是無主引用。

注意:
Swift 有以下要求:只要在閉包內使用self的成員,就要用self.someProperty或者self.someMethod(而不僅是somePropertysomeMethod)。這提醒你可能會一不當心就捕獲了self

定義捕獲列表

捕獲列表中的每一項都由一對元素組成,一個元素是weakunowned關鍵字,另外一個元素是類實例的引用(如self)或初始化過的變量(如delegate = self.delegate!)。這些項在方括號中用逗號分開。

若是閉包有參數列表和返回類型,把捕獲列表放在它們前面:

swiftlazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

若是閉包沒有指明參數列表或者返回類型,即它們會經過上下文推斷,那麼能夠把捕獲列表和關鍵字in放在閉包最開始的地方:

swiftlazy var someClosure: Void -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

弱引用和無主引用

在閉包和捕獲的實例老是互相引用時而且老是同時銷燬時,將閉包內的捕獲定義爲無主引用。

相反的,在被捕獲的引用可能會變爲nil時,將閉包內的捕獲定義爲弱引用。弱引用老是可選類型,而且當引用的實例被銷燬後,弱引用的值會自動置爲nil。這使咱們能夠在閉包體內檢查它們是否存在。

注意:
若是被捕獲的引用絕對不會變爲nil,應該用無主引用,而不是弱引用。

前面的HTMLElement例子中,無主引用是正確的解決循環強引用的方法。這樣編寫HTMLElement類來避免循環強引用:

swiftclass HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: Void -> 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實例:

swiftvar paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"

使用捕獲列表後引用關係以下圖所示:

這一次,閉包以無主引用的形式捕獲self,並不會持有HTMLElement實例的強引用。若是將paragraph賦值爲nilHTMLElement實例將會被銷燬,並能看到它的析構函數打印出的消息。

swiftparagraph = nil
// prints "p is being deinitialized"
相關文章
相關標籤/搜索