読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(12) - Deinitialization、Automatic Reference Counting

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Deinitialization(デイニシャライザ)

所謂デストラクタと呼ばれる概念のものです。

この手の機構はどの言語もそれぞれ癖のある動きをしがちです。大概「オブジェクトの寿命が尽きた(ローカル変数がスコープから抜けたetc)」とか、「GCによる回収直前(この動きはどちらかと言うとファイナライザと呼ばれる)」とか、起動する条件が微妙に違う上に大体同じところで起きてしまうのが原因なんですけどね。

Swiftはどう言ったタイミングで発動するのか、そもそもそこまでちゃんと書いてあるのか。を、これから読んでいきましょう。

そうそう、大事なことを言い忘れていました。デイニシャライザはクラスインスタンスにのみ設定できます。

How Deinitialization Works

Swiftのクラスインスタンスはautomatic reference counting (ARC)と言う機構でメモリ管理されています。そのおかげで基本的には明示的にメモリ解放するようなコードを書かなくても問題ありません。ただまぁ、皆さんご存知の通り、そうもいかないものが世の中にはいっぱいあります。(ネイティブなあれとか、ネイティブなあれとか、ネイティブなあれです。)

そう言うものを扱うインスタンスではデイニシャライザを記述してちゃんと明示的に解放してあげましょう。構文は以下の通りです。

deinit {
    // perform the deinitialization
}

デイニシャライザはインスタンスが解放される直前に自動で呼び出されます。(いやらしい書き方だ…。原文:Deinitializers are called automatically, just before instance deallocation takes place.)

これを明示的に呼ぶことは出来ません。また、親クラスのデイニシャライザは子クラスから必ず呼ばれます。子クラスにデイニシャライザを記述しなかったとしてもです。呼ばれる順番は子クラスのデイニシャライザ→親クラスのデイニシャライザです。

そのおかげでデイニシャライザ内では全てのプロパティのメモリが割り当てられたままになっていますし、それを参照してデイニシャライザ内の処理を分岐させても大丈夫なことが保障されています。「いやそれ当たり前でしょ?」って言える日がいつか来るといいですね。(遠い目)

Deinitializers in Action

デイニシャライザを使ったサンプルコードです。

struct Bank {
    static var coinsInBank = 10_000
    
    static func vendCoins(var numberOfCoinsToVend: Int) -> Int {
        numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank)
        coinsInBank -= numberOfCoinsToVend
        return numberOfCoinsToVend
    }
    
    static func receiveCoins(coins: Int) {
        coinsInBank += coins
    }
}

class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.vendCoins(coins)
    }
    
    func winCoins(coins: Int) {
        coinsInPurse += Bank.vendCoins(coins)
    }
    
    deinit {
        Bank.receiveCoins(coinsInPurse)
    }
}

Playerは初期化時、あるいは何らかの勝負に勝利するとBankからお金を借り(vendCoins)、所持することが出来ます。Playerのインスタンスの寿命が尽きたら、Bankにお金を返します(receiveConins)。

// Playerの初期化
var playerOne: Player? = Player(coins: 100)

println("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// prints "A new player has joined the game with 100 coins"

println("There are now \(Bank.coinsInBank) coins left in the bank")
// prints "There are now 9900 coins left in the bank"


// Player#winCoins
playerOne!.winCoins(2_000)

println("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// prints "PlayerOne won 2000 coins & now has 2100 coins"

println("The bank now only has \(Bank.coinsInBank) coins left")
// prints "The bank now only has 7900 coins left"


// PlayerOneにnilをセット
playerOne = nil

println("PlayerOne has left the game")
// prints "PlayerOne has left the game"

println("The bank now has \(Bank.coinsInBank) coins")
// prints "The bank now has 10000 coins"

Automatic Reference Counting(自動参照カウント)

Deinitializationでも言いましたが、Swiftインスタンスにおけるメモリ管理は基本的にautomatic reference counting (ARC)と呼ばれる機構により行われます。(Objective-Cにもあるらしい。)

ただし、Classes and Structuresの時にもちょろっと触れましたが、参照カウントを用いているのはクラスだけです。

値型のインスタンスのメモリはどうなってるんでしょうかね?その辺は専門外なので言及は避けておきます。

How ARC Works

ARCはあるクラスのインスタンスが作成されるとある程度の大きさのメモリ(a chunk of memory)に格納します。このメモリには型情報とプロパティの情報が格納されています。そしてそのインスタンスが不要になると自動でメモリを解放してくれます。

当然ながらインスタンスのメモリを解放してしまえばプロパティやメソッドにアクセスできなくなってしまうので、そのインスタンスが確実に不要であることを知るために、プロパティや定数 / 変数がどのように参照されているかを追跡しています。ARCは一つでもその参照が生きていれば決してメモリを解放することはありません。この参照のことを「強参照(strong reference)」と呼びます。

要するに、この章は強参照とか循環参照とか弱参照の、その辺のお話なわけです。

ARC in Action

参照カウントに関する説明です。以下のようなクラスがあったとします。

class Person {
    let name: String

    init(name: String) {
        self.name = name
        println("\(name) is being initialized")
    }
    
    deinit {
        println("\(name) is being deinitialized")
    }
}

このPersonクラスのインスタンスを三つ作成してみましょう。

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

この時点ではOptionなので全部nilです。Personクラスのためのメモリはまだ確保されていません。

じゃあ実際にPersonクラスのインスタンスを割り当ててみましょう。

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

この時点でPersonには一つの強参照がある、と言えます。次はreference2とreference3にreference1の参照を代入し、reference1とreference2にnilを代入してみましょう。

reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil

この時点ではデイニシャライザの処理は実行されません。reference3がPersonに対する強参照として生き残っているためです。reference3にもnilを代入して始めて強参照がなくなり、デイニシャライザの処理が実行されます。

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

Strong Reference Cycles Between Class Instances

上記の例で見た通り、一つでも強参照が残っていればクラスのメモリは絶対に解放されません。

ただ、ある二つのクラスがお互いに強参照を持ち続けてしまうと、実際には到達不可能なコードであるにも関わらずメモリが解放されないケースがあります。所謂「循環参照(strong reference cycle)」と呼ばれるものです。

例えばこんなクラスがあったとします。

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 }

    var tenant: Person?
    deinit { println("Apartment #\(number) is being deinitialized") }
}

Personクラスには「var apartment: Apartment?」と言うプロパティが、Apartmentクラスには「var tenant: Person?」と言うプロパティがあります。まぁこのぐらい単純な例だと見た瞬間どうなるかわかるんですけど、ちゃんと説明しておきましょう。

var john: Person?
var number73: Apartment?

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

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

john = nil
number73 = nil

今この状態ではPerson、Apartment両方のデイニシャライザが呼び出されません。

確かにjohnはnilになりましたが、Personクラス内のapartmentはまだ残っています。なのでApartmentクラスの強参照が残り続けています。

number73もnilです。でもApartmentクラスのtenantはnilになっていないので、Personクラスの強参照が残っています。

そんなわけで、こんな単純な例でもメモリリークしてしまいます。実際のコードになればもっともっと複雑になるでしょう。容易く起きる上に中々見つけにくいのが循環参照の厄介なところです。

Resolving Strong Reference Cycles Between Class Instances

こんなことが起こらないようにするために、Swiftでは「弱参照(weak reference)」と「非所有参照(unowned reference)」が用意されています。

弱参照、非所有参照の使い分け方ですが、前者は「どこかのタイミングでnilになりうる参照」に、後者は「初期化後にnilになることがないことがわかっている参照」に対して使用します。

まずは弱参照の例から。先ほどのApartmentクラスを書き換えます。

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") }
}

弱参照にしたい場合はvarの前に「weak」とつけます。letの前にはつけられません。弱参照の役割からしても当然ですね。

これでjohnをnilにすればPersonクラスが解放されるようになりました。

var john: Person?
var number73: Apartment?

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

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

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

「でもPersonのapartmentはnilになってないんじゃないの?」と思うかもしれませんが、Personが解放される=中のプロパティの情報におけるメモリが解放される、と言うことでもあるので、johnがnilになった時点でApartmentクラスに対する強参照は一つだけです。

その残った一つであるnumber73をnilにしてやれば、Apartmentも問題なく解放出来ます。

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

じゃあ次は非所有参照の例を見てみましょう。弱参照と同じように「unowned」をつけます。

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") }
}

弱参照と違ってletでも使えます。これも役割を考えたら当然です。

じゃあ参照をつけていきましょう。

var john: Customer?

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

ちょっと整理しましょう。Customerクラスへの参照はjohn(強参照)とCreditCard#customer(非所有参照)です。CreditCardへの参照はjohn#card(強参照)のみとなります。

ここでjohnをnilにしてみます。そうするとCustomerクラスへの強参照がなくなるので解放されます。Customerクラスが解放されると、CreditCardクラスへの強参照も自動的に消滅します。よってCreditCardクラスも同時に解放されます。

john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"

これ、CreditCardのデイニシャライザでcustomerにアクセス出来るんですかね?

まぁ何にせよ、弱参照や非所有参照として宣言されているものは処理の流れの中で唐突にメモリ上から消滅していることがありえます。その辺のことも考えて使いましょう。

もう一つ、若干複雑な例もあげておきましょう。正直このコードがなぜ非所有参照の流れで出てくるのかはいまいちわかりませんが…。

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
    }
}

イニシャライザの説明の際に触れましたが、イニシャライザには二段階あり、その二段階目でなければイニシャライザでselfを使うことは出来ません

なのでCountryのイニシャライザでCityのインスタンスを作成するためにselfを渡すことは出来ません。(原文:the initializer for Country cannot pass self to the City initializer until a new Country instance is fully initialized)

イニシャライザのルールとして、変数宣言の時点でデフォルト値が与えられていれば問題ありません。と言うことでcapitalCityを一旦自動でnilがセットされるOptionにし、イニシャライザで改めて値をセットすると言う形を取るようにします。

そこで、型としてはOptionなんだけど確実に値が入っていることが暗黙的に保障されているImplicitly Unwrapped Optionalsの出番です。

以前紹介したので細かい話は割愛しますが、呼び出し側はまるでOptionではないかのように扱うことが出来ます。

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"

非所有参照の話の流れでこれが出てくるのは、Implicitly Unwrapped Optionalsを使うと便利なケースは大概循環参照になりえるからですかね?

(正直イニシャライザ云々の話は全然自信がないのであんまり鵜呑みにしないでください…。)

Strong Reference Cycles for Closures

プロパティだけでなく、プロパティをセットするためのクロージャでも循環参照に気をつけなければなりません。

class HTMLElement {
    let name: String
    let text: String?

    @lazy var asHTML: () -> 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 {
        println("\(name) is being deinitialized")
    }
}

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

// paragraphをnilにしてもデイニシャライズは呼び出されない
paragraph = nil

上記の例は循環参照が起きてしまっています。どこに強参照が残っているのでしょう?

これはasHTMLのクロージャで呼び出している「self」です。これがHTMLElementへの強参照となっているせいで、メモリを解放することが出来ません。

もうちょっと詳しく説明すると、asHTMLは「() -> String」と言う型を持っています。クロージャは参照型なので、HTMLElementは「() -> String」に対する強参照を所持していることになってしまいます。そして先ほども述べた通り「() -> String」はHTMLElementへの強参照を持っているので、この参照も外さないとHTMLElementの解放が出来ません。と言うわけで、これは循環参照です。

Resolving Strong Reference Cycles for Closures

じゃあどうすればいいのかと言うと、クロージャ内におけるself(= HTMLElement)が非所有参照になれば問題ないはずです。(selfがnilになることはありえないので、弱参照は使えない。)

そのためにこんなおまじない的な構文が用意されています。

// 引数に名前を与えるケース
@lazy var someClosure: (Int, String) -> String = {
    [unowned self] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

// 省略形
@lazy var someClosure: () -> String = {
    [unowned self] in
    // closure body goes here
}

先ほどの例を書き直しましょう。

class HTMLElement {
    let name: String
    let text: String?

    @lazy var asHTML: () -> 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 {
        println("\(name) is being deinitialized")
    }
}

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

paragraph = nil
// prints "p is being deinitialized"

これでクロージャ内のself(HTMLElement)への参照が非所有参照となり、paragraphをnilにすることでHTMLElementへの強参照が消滅、無事にデイニシャライザが呼ばれました。