Swiftの終了処理と終了子(Deinitialization)

Swiftの終了子 | Deinitialization

初期化との対比で書けば、「Deinitialization」は「終了化」またはもっと分かりやすく言うと「終了処理」でしょうか?Swiftにおける終了処理は、インスタンスのメモリが解放される際の処理のことで、基本的には自動です(automatic reference counting)。何かしらマニュアルで操作が必要な場合に終了子(deinitializers)が必要になります。

ここでは、終了子とは何か?から始めて、終了子の定義構文とサブクラスでの挙動を説明し、さらに具体例を使って終了子の働きについて詳しく説明します。

終了子(Deinitializers)って何?

終了子(Deinitializers)とは、その名前の通り、

クラスインスタンスがメモリを解放する直前に呼び出される機能

です。

ざっくり言うと、初期化子(initializers)インスタンス(instances)を「作る」のに対して、終了子はインスタンスを「破棄(解放)」するための機能です。

Deinitializationの頭についている「de」は「下降」や「否定」、「反対」など、ネガティブな意味合いを言葉に付与する接頭辞です。終了子なら「Terminators」とか「Finalizers」とかでも良いような気もしますが、初期化子との機能的な対比という観点では「Deinitializers」は分かりやすいかもしれません。

なんとなく予想は付くと思いますが、終了子の定義構文はdeinitキーワードを使います。

deinit {
  // 終了プロセスの実行
}

最初に書きましたが、終了子はクラス限定の機能ですから、構造体(structures)列挙型(enumerations)では使えません。

終了子は何をする?

基本的にメモリ管理はSwiftが自動的に行う

Swiftでは、基本的にインスタンスのメモリ解放は自動的に行われます。インスタンスのメモリ管理はAutomatic Reference Counting (ARC)と呼ばれる機能で行われますが、これは別ページで詳しく説明します。日本語にすると「自動参照カウント」でしょうか?したがって、普通は自分の作ったインスタンスのメモリを解放するために、何かしらの操作を行う必要はありません。

しかし、自動的にメモリが開放されないケースもあるかもしれません。公式マニュアルにも書いてありますが、例えば外部ファイル等を自作クラスから操作する場合、が考えられます。その場合、外部ファイルをクラスで操作するために割り当てたメモリは、それが必要なくなった時に自分で解放する必要があるかもしれません。

終了子の定義構文

クラス(classes)は1つしか終了子を持つことが出来ません。これは何個でも実装できる初期化子とは違いますね。また、終了子はパラメータを持ちませんので、カッコ()を書きません。これは最初に定義構文を紹介した通りですが、重要なのでもう1度書いておきます。

deinit {
  // 終了プロセスの実行
}
パラメータを取れないから1つしか実装できない、と考えるのが妥当かもしれません。もしくは、1つしか実装させないために、パラメータを取れないようにした、かもしれませんが。そもそもパラメータが必要ないということかもしれません。

派生先におけるスーパークラスの終了子の挙動

終了子は自動的に呼び出されるので、「このタイミングで呼び出したい!」という風に自分から呼び出すことは出来ません。呼び出されるタイミングは決まっていて、メモリが解放される時点です。

また、クラスは継承可能なので、スーパークラス(superclass)の終了子が、サブクラス内でどのように取り扱われるかのルールがあります。

  • スーパークラスの終了子はサブクラスに継承される
  • スーパークラスの終了子はサブクラスの終了子の最後で、自動的に呼び出される
  • サブクラスが終了子を持っていなくても、スーパークラスの終了子は呼び出される

では、実際にコードを書いてテストしてみます。

// 終了子のテスト
// 基底クラス
class Base {
    var name: String?
    init(name: String) {
        self.name = name
        print("Baseクラスを初期化: \(name)")
    }

    deinit {
        print("Baseクラスを終了: \(name!)")
        self.name = nil
    }
}

// Baseを継承したサブクラス
// 終了子は継承される
class Sub: Base {
    let subName = "subclass"
}

// テスト
print("1. オプショナルサブクラスを作成")
var sub: Sub? = Sub(name: "基底クラス")

print("2. nilを代入")
sub = nil

// Baseを継承した別のサブクラス
// スーパークラスの終了子がどう呼び出されるか?
class AnotherSub: Base {
    var anotherName: String?
    init() {
        self.anotherName = "sub class"
        super.init(name: "base class")
        print("AnotherSubクラスを初期化: \(anotherName!)")
    }

    deinit {
        print("AnotherSubクラスを終了: \(anotherName!)")
        self.anotherName = nil
    }
}

print("1. オプショナルサブクラスを作成")
var anotherSub: AnotherSub? = AnotherSub()

print("2. nilを代入")
anotherSub = nil

ちょっと長いですが、そんなに複雑ではありません。順番に見ていきます。

基底クラスで終了子を定義

わざわざ格納プロパティ(stored property)を準備しなくても良かったのですが、メモリ解放というのを擬似的に表現するのにオプショナル型(optionals)を使っています。

// 基底クラス
class Base {
    var name: String?
    init(name: String) {
        self.name = name
        print("Baseクラスを初期化: \(name)")
    }

    deinit {
        print("Baseクラスを終了: \(name!)")
        self.name = nil
    }
}

後でテストすることを考えて、初期化子と終了子にprint(_:)関数を挟んでいます。

終了子の継承

先ず、最初のサブクラスでは終了子を定義していません。

// Baseを継承したサブクラス
// 終了子は継承される
class Sub: Base {
    let subName = "subclass"
}

このサブクラスは、

  • 終了子が本当に継承されているか(ルール1)
  • サブクラスで終了子が無くてもスーパークラスの終了子が呼び出されるか(ルール3)

というテストを行うためのものです。実際にテストすると分かりますが、

// テスト
print("1. オプショナルサブクラスを作成")
var sub: Sub? = Sub(name: "基底クラス")

print("2. nilを代入")
sub = nil

// 結果
//1. オプショナルサブクラスを作成
//Baseクラスを初期化: 基底クラス
//2. nilを代入
//Baseクラスを終了: 基底クラス

となり、サブクラスからスーパークラスの終了子が呼び出されることが分かります。このように、終了子は暗黙に継承されて、派生先で終了子がない場合でも呼び出されることが確認できます。

サブクラスで終了子を定義

次の例では、サブクラスで終了子を定義して、「スーパークラスの終了子がサブクラスの終了子の最後から呼び出されるか?」(ルール2)をテストします。

// Baseを継承した別のサブクラス
// スーパークラスの終了子がどう呼び出されるか?
class AnotherSub: Base {
    var anotherName: String?
    init() {
        self.anotherName = "sub class"
        super.init(name: "base class")
        print("AnotherSubクラスを初期化: \(anotherName!)")
    }

    deinit {
        print("AnotherSubクラスを終了: \(anotherName!)")
        self.anotherName = nil
    }
}

このサブクラスでは終了子を定義しています。ルール2の条件に従うと、順番としては(1)サブクラスAnotherSubの終了子、(2)スーパークラスBaseの終了子、という順序で呼び出されるはずです。

実際にテストしてみると、

// テスト
print("1. オプショナルサブクラスを作成")
var anotherSub: AnotherSub? = AnotherSub()

print("2. nilを代入")
anotherSub = nil

//結果
//1. オプショナルサブクラスを作成
//Baseクラスを初期化: base class
//AnotherSubクラスを初期化: sub class
//2. nilを代入
//AnotherSubクラスを終了: sub class
//Baseクラスを終了: base class

という具合で、確かに期待通りに終了プロセスが実行されているのが分かります。

(実際のプログラムとしては機能しませんが)この終了プロセスを明示的に書くと

// 実際の終了プロセスのイメージ
deinit {
    print("AnotherSubクラスを終了: \(anotherName!)")
    self.anotherName = nil

    // 一番最後にスーパークラスの終了子
    super.deinit()
}

という感じでしょうか?

終了子の中身を見てもらうと分かるかもしれませんが、終了子はインスタンスが保有する全てのプロパティにアクセスすることが出来ます。これは、メモリ解放のタイミングが「終了子呼び出しの後」だからです。

具体例で理解する終了処理 | BankPlayer

公式マニュアルにある具体例を使って、もう少し終了子deinitの働きを理解しようと思います。

// お金の出し入れが可能な「銀行」構造体
struct Bank {
    static var coinsInBank = 10_000
    static func distribute(coins numberOfCoinsRequested: Int) -> Int {
        let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
        coinsInBank -= numberOfCoinsToVend
        return numberOfCoinsToVend
    }
    
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

// プレイヤークラス
class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }
    
    func win(coins: Int) {
        coinsInPurse += Bank.distribute(coins: coins)
    }
    
    func check() {
        print("手持ちのコイン:\(coinsInPurse), 銀行のコイン:\(Bank.coinsInBank)")
    }
    
    deinit {
        Bank.receive(coins: coinsInPurse)
    }
}

公式では、Bankはクラスでしたが、ここでは構造体にしています。特に理由はありませんが、Swiftでは基本的な関数機能等を構造体でも実装可能なことが、この例で分かると思います。また、分かりやすくするために、Bankが管理するコインの単位を円と仮定して話をします。また、コインの数をチェックするメソッドcheck()Playerに追加しています。

2つの独自型BankPlayerは、何かしらのゲームを通じてコインを獲得する機能を実装したものです。Bankはコインを管理し、それをPlayerに渡したり、Playerから受け取ったりするための型です。Playerはその名前の通り、実際にゲームに参加する人を想定しています。

型プロパティと型メソッドで共通機能を実装 | Bank構造体

Bank構造体をもう一度載せておきます。

// お金の出し入れが可能な「銀行」構造体
struct Bank {
    static var coinsInBank = 10_000
    static func distribute(coins numberOfCoinsRequested: Int) -> Int {
        let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
        coinsInBank -= numberOfCoinsToVend
        return numberOfCoinsToVend
    }
    
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

少し詳しく見ていきます。

なぜ型プロパティと型メソッド?

今このゲーム内では、コインの管理をするBankは1つだけしか存在しないという設定で、コインの上限値は10000円にセットしてあります。

見ると分かると思いますが、Bankが保有する全てのプロパティとメソッドが型プロパティ(type properties)型メソッド(type methods)で実装されています。これは

コインの管理をするBankは1つだけしか存在しないという設定

を満たすための実装です。そうしないと上限の10000円を設定した意味がありません。

もし、普通のインスタンスプロパティやメソッドで実装した場合、インスタンスを生成する度にBankに+10000円ということになります。例えば、Bankインスタンスを3つ作るとトータル30000円保持するBank(群)を持つことになり、上限10000円を設定したことと矛盾します。

Bankは型メソッドを2つ持っていて、1つはコインを(Playerに)渡すdistribute(coins:)、もう1つはコインを(Playerに)受け取るreceive(coins:)です。

Swift 2.xまではdistributeメソッドはvendCoinsという名前でした。「Distribute」というのは「供給する」とか「配布する」という意味です。以前使われていたvendは「〜を販売する」という意味で使われます。自動販売機を英語では「vending machine」と言います。また、receiveは「(物を)受け取る」等の意味があります。バレーボールの「レシーブ」もこの単語です。

Distributeメソッド

receive(coins:)は簡単なので説明の必要はないと思います。受け取ったパラメータを、自身が持っているコインに加えているだけです。distribute(coins:)は少し複雑です。

static func distribute(coins numberOfCoinsRequested: Int) -> Int {
    let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
    coinsInBank -= numberOfCoinsToVend
    return numberOfCoinsToVend
}

メソッド全体は上記のようになっていますが、基本的には要求されたコインの数(numberOfCoinsRequested)を返すメソッドです。

static func distirbute(coins numberOfCoinsRequested: Int) -> Int {
    ....
    return numberOfCoinsToVend
}

戻り値の名前が違いますが、これはBankが持っているコインの数が有限だからです。もし上限がないなら、要求されたコインをそのまま返せばいいだけの話です。メソッド内部で実行している処理で、渡すコインの数を制限しています。

let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberOfCoinsToVend

1行目は、最終的な戻り値であるnumberOfCoinsToVendを計算しています。これは、numberOfCoinsRequestedcoinsInBankを比較して小さい方を採用する、という処理で、組み込み関数min(_:_)を使って実行しています。条件分岐のif文を使えば

if numberOfCoinsRequested < coinsInBank {
    // 銀行にはまだお金が余っているのでリクエストされたままの金額を渡す
    return numberOfCoinsRequested
} else {
    // 要求されたお金が銀行にないので、銀行に残っているありったけのお金を渡す
    return coinsInBank
}

という感じです。

2行目は、銀行から引き出すコイン(numberOfCoinsToVend)を総額(coinsInBank)から引いています。後で具体例を示しますが、それを見るとdistribute(coins:)メソッドの動きが良く分かると思います。

またdistribute(coins:)メソッドはパラメータ名として

static func distribute(coins numberOfCoinsRequested: Int) -> Int {

のように、外部呼び出し用ラベルにcoins、メソッド定義用内部パラメータ名にnumberOfCoinsRequestedをセットしています。

Playerクラス

次はPlayerクラスです。

// プレイヤークラス
class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }

    func win(coins: Int) {
        coinsInPurse += Bank.distribute(coins: coins)
    }

    func check() {
        print("手持ちのコイン:\(coinsInPurse), 銀行のコイン:\(Bank.coinsInBank)")
    }

    deinit {
        Bank.receive(coins: coinsInPurse)
    }
}

Playerクラスでは、Bank構造体を使ってコインのやりとりを行っています。初期化子、メソッド、終了子を持っているので、それぞれ順番に説明します。

要求したコインを受け取れる(かもしれない)初期化子

初期化子はパラメータcoinsを使って、銀行から好きなだけコインを受け取れます。

class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }
....
}

ただし、先程説明したように、distribute(coins:)メソッドはBankが保有するコインの総数で上限を押さえられてるので、要求したコインの数だけコインがもらえるかどうかは分かりません。

実際に試してみると分かりますが、

let playerOne: Player? = Player(coins: 2_000)
playerOne!.check()

let playerTwo: Player? = Player(coins: 5_000)
playerTwo!.check()

let playerThree: Player? = Player(coins: 4_000)
playerThree!.check()

//結果
//手持ちのコイン:2000, 銀行のコイン:8000 <-- playerOne
//手持ちのコイン:5000, 銀行のコイン:3000 <-- playerTwo
//手持ちのコイン:3000, 銀行のコイン:0    <-- playerThree

最初の2人は要求通りのコインを受け取りましたが、その時点で銀行に残ったコインは3000円です。したがって、3人目は4000円要求したのにも関わらず、3000円しかもらえていません。という感じで、実装した機能は期待通り動いていることが分かります。

コインを受け取る(受け取れるかもしれない)メソッド | win(coins:)

win(coins:)は銀行からコインを受け取るためのメソッドです。

func win(coins: Int) {
    coinsInPurse += Bank.distribute(coins: coins)
}

受け取ったコインは、Playerが保有するプロパティcoinsInPurseに追加されます。

Purseは「財布」ですが、小銭入れというニュアンスのようです。普通の「財布」はwalletでしょうか。

機能としては初期化子と一緒ですが、メソッドなのでインスタンスを作るといつでも呼び出せます。

コインを全額銀行に戻す終了子

終了子はBankreceive(coins:)を使って、プレイヤーが持っているコインを全額銀行に戻す処理を行っています。この終了子によって「プレイヤーがゲームを抜ける」ということを表現しています。

deinit {
    Bank.receive(coins: coinsInPurse)
}

実際に、Playerインスタンスを生成して、終了子でインスタンスを破棄するまでのコインの動きを見てみます。

var playerOne: Player? = Player(coins: 2_000)
playerOne!.check()
playerOne!.win(coins: 600)
playerOne!.check()
playerOne = nil
print("銀行のコイン:\(Bank.coinsInBank)")

//結果
//手持ちのコイン:2000, 銀行のコイン:8000
//手持ちのコイン:2600, 銀行のコイン:7400
//銀行のコイン:10000

最終的に手でインスタンスを破棄したいので、オプショナル型を使ってplayerOneインスタンスを作っています。こうするとnilを代入した時点で終了子を呼び出せます。

今、初期化子で2000円コインを受け取っていますので、

var playerOne: Player? = Player(coins: 2_000)
playerOne!.check()
//手持ちのコイン:2000, 銀行のコイン:8000

当然銀行には8000円残っています。また、オプショナル型へのアクセスにはforced unwrappingを使っています。次に、win(coins:)メソッドでさらに600円受け取りました。

playerOne!.win(coins: 600)
playerOne!.check()
//手持ちのコイン:2600, 銀行のコイン:7400

そうすると、手持ちが2600円で銀行には7400円残っています。最後に、ゲームを抜けるため、クラスインスタンスにnilを代入します。

playerOne = nil
print("銀行のコイン:\(Bank.coinsInBank)")
//銀行のコイン:10000

nilを代入した時点で終了子が呼び出されますので、そこでplayerOneが持っていたコインが全て銀行に戻ります。上で確認しているように、確かに全額銀行に戻っていることが分かります。

まとめ

  • 終了子(Deinitializers)はクラスインスタンスがメモリを解放する直前に呼び出される
  • 終了子はクラス限定の機能
  • 終了子はクラスに1つしか実装できない。また、パラメータを取らない
  • スーパークラスの終了子はサブクラスで継承される
  • サブクラスでは、スーパークラスの終了子が何かしらの形で呼び出される