Swiftの初期化 | クラスの初期化子の委譲(Initializer Delegation for Classes)

クラスの初期化の委譲(Initializer Delegation for Classes)

クラスは継承できるので、その初期化プロセスは構造体などに比べると複雑になります。クラスの持つ格納プロパティは、スーパークラスの持つモノも含めて、初期化の際に全て初期値を与えないといけません。それらをどういう経路で初期化するのか?ということは、実は明確に決まっています。初期化子の委譲を詳しく見ていくと、この「どういう経路で初期化するのか?」ということが良く分かると思います。

このページでは、クラス型のための2つの初期化子(designated initializerとconvenience initializer)から始めて、クラス型の初期化プロセス、さらに2段階初期化に関して詳しく説明していきます。

概観

Swiftはクラス(classes)用に2つ初期化子を用意していて、

  • designated initializersと、
  • convenience initializers
  • です。初期化子が2種類ある理由については,この後詳しく見ていきます。簡単に言うと、継承(inheritance)があるために役割分担が必要で、それを実行するのに「2つあった方が便利じゃね?」ということです。

    「Designate」は「指定する」とか「指名する」とか「指示する」等の意味がありますが、特に「特定の目的のために、公式に人や物を選ぶ」という意味合いで使われます。敢えてdesignated initializersの日本語訳を考えると、「クラス初期化のための初期化子」という感じでしょうか?

    また、「convenience」は「便利」とか「好都合」という訳ですが、「コンビニエンス・ストア」の「コンビニエンス」が、この単語です。後で説明しますが、convenience initializersはサポート用の初期化子なので、まさに「必要な時に用意する便利な初期化子」という意味合いです。

    簡単にまとめると

    初期化子を作る場合、designated initializerは必須、convenience initializerは任意(あってもなくても良い)。

    ということです。

    Default initializerがあるので、初期化子自体は(もしそれが必要でなければ)定義しなくても問題ありません。

    Designated InitializersとConvenience Initializersの定義構文

    Designated initializersの定義構文は、値型の初期化子の定義構文と一緒です。

    init(parameters){
        statements
    }
    

    Convenience initializersの場合、基本構造は同じですが、initの前にconvenienceキーワードが必要です。

    convenience init(parameters){
        statements
    }
    

    convenienceinitの間には半角スペースが必要なので注意です。

    これらの初期化子の役割に関して、以下で少し詳しく見ていきます。

    Designated Initializersとは?

    先程も述べたように、designated initializersはクラスにとってメインの初期化子です。Designated initializerの役割は、大きく分けると2つあります。

    • クラスの保有する全てのプロパティを初期化
    • スーパークラスのdesignated initializerを呼び出す

    上の2つの条件から、複数階層の継承チェインでも、designated initializerで全てのプロパティを初期化出来ることが分かります。

    全てのクラスは少なくとも1つdesignated initializerを持たないといけません。後で詳しく見ますが、スーパークラスからdesignated initializerを継承した場合、自動的にdesignated initializerを持つことになります(これをautomatic initializer inheritanceと呼びます、詳しくは別のページで)。

    Convenience Initializersとは?

    Convenience initializersはサポート用の初期化子です。Convenience initializerは、最終的には必ずdesignated initializerを呼びださなければいけません。

    先程述べましたが、convenience initializersはあくまでサポート用ですから、必要なければ用意しなくても大丈夫です。初期化の過程で、何かしらの共通機能が出て来た場合は、convenience initializerを作ってカプセル化すると便利ですし、その意図が明確になります。

    クラス型のInitializer Delegation

    初期化子委譲の3つのルール

    公式マニュアルに書いてある「delegationコールの3つのルール」は非常に分かりやすいので、そのまま和訳してみます。

    • ルール1: Designated initializerは、直接継承しているスーパークラスのdesignated initializerを呼びださなければならない
    • ルール2: Convenience initializerは、同じクラスの別の初期化子を呼びださなければならない
    • ルール3: Convenience initializerは、最終的にはdesignated initializerを
      呼びださなければならない

    さらに分かりやすく書いていますが、意訳してみると

    • Designated initializerは、上に委譲(delegate up)
    • Convenience initializerは、横に委譲(delegate across)

    図解も非常に分かりやすいですので、同じものを載せます。この図を見ると、designated initializerは「上に委譲」して、convenience initializerは「横に委譲」しているのが、ひと目で分かります。

    [図解]クラスのinitializer delegation

    クラスの初期化子委譲の具体例

    これだけでも説明可能ですが、実際にクラスを作って試してみます。

    // Designated and convenience initializers
    class SuperClass {
        var property = 0
        var name = "some superclass"
        init(property: Int, name: String) {
            self.property = property
            self.name = name
        }
        convenience init(name: String) {
            self.init(property:1, name:name + " convenience1")
        }
        convenience init() {
            self.init(name: "superclass convenience2")
        }
    }
    
    class SubClass: SuperClass {
        init(property: Int) {
            super.init(property:property, name:"subclass")
        }
        init(name: String) {
            super.init(property:1, name:name)
        }
        convenience init() {
            self.init(name: "subclass convenience")
        }
    }
    
    let superClass = SuperClass()
    let subClass = SubClass()
    print(superClass.name)
    print(subClass.name)
    

    クラスの機能は適当で、delegationの部分だけを再現するように設計しています。

    スーパークラス(superclass)はdesignated initializerを1つ、convenience initializerを2つ持っています。最後のconvenience initializer(convenience init())は、別のconvenience initializer(convenience init(name: String))を呼び出していて、今度はそれがdesignated initializer(init(property: Int, name: String))を呼び出しています(ルール2と3)。スーパークラスはこれ以上継承先がないので、ルール1は適用されません。

    サブクラス(subclass)はdesignated initializerを2つ、convenience initializerを1つ持っています。ルール3を満たすためには、convenience initializer(convenience init())はdesignated initializersのどちらかを呼び出さないといけません。上の例だと、2番目のdesignated initializerを呼び出しています。この時点で、ルール2と3は満たしています。また、2つあるdesignated initializersはどちらも、スーパークラスのdesignated initializerをsuper.init(...)で呼び出しています(ルール1)。

    公式マニュアルに注意書きがありますが、ここで説明したことは、クラスを書く側に対するルールです。したがって、ユーザ(クラスを使う、もしくはクラスインスタンスを生成する側)には全く関係ない問題で、ユーザは提示されたどの初期化子を使ってクラスを初期化しても構いません。

    “NOTE

    These rules don’t affect how users of your classes create instances of each class. Any initializer in the diagram above can be used to create a fully-initialized instance of the class they belong to. The rules only affect how you write the implementation of the class’s initializers.”

    抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

    2段階初期化(Two-Phase Initialization)

    Swiftにおけるクラスの初期化は、2段階のプロセスです。1段階目では、クラスの持つ各格納プロパティに初期値が与えられます。一旦全ての格納プロパティの初期状態が決まると、2段階目が開始されます。2段階目では、各クラスが各々持っている格納プロパティをさらにカスタマイズすることが出来ます。この2段階プロセスが終了して始めて、新しいインスタンスを作る用意が出来ます。

    [図解]2段階初期化の簡略化した概念図

    上の図のように、Swiftにおけるクラスの初期化を簡単にまとめると

    • 1段階目:全てのプロパティの初期化
    • 2段階目:(必要なら)プロパティのカスタマイズ

    となります。

    なぜ2段階初期化?

    なぜ2段階の初期化を採用しているのか?と言うと、

    • 初期化を安全に実行しつつ(1段階目)
    • クラス階層における各クラスに、初期化をカスタマイズする柔軟性を与えるため(2段階目)

    です。2段階の初期化は、初期化されていないプロパティへのアクセスを未然に防ぐのと同時に、プロパティが別の初期化子で(予期せず)異なる値にセットされることも防げます。

    Object-Cでは、初期化の際の初期値が0またはnilで固定だったようですが、Swiftでは任意の値を取ることが可能です。

    4つ安全確認(safety-checks)

    Swiftのコンパイラは、2段階初期化が正常に完了するように、4つの安全確認(safety-checks)を行います。基本的に、この確認は1段階目の初期化プロセスそのものです。4つ目だけは、2段階目の初期化とも関わっています。

    Safety check 1: 全プロパティの初期化

    Designated initializerは、スーパークラスの初期化子に委譲(上に委譲、delegate up)する前に、そのクラスが保有する全てのプロパティを確実に初期化しなければならない

    Designated initializerの最も重要な仕事は、クラスが保有するプロパティを確実に初期化することです。クラスが持つ全ての格納プロパティの初期状態が確定しないと、オブジェクトのメモリが割り当てられることはないので、designated initializerによる初期化は重要です。

    Safety check 2: 上に委譲

    Designated initializerは、継承したプロパティに値を割り当てる前に、スーパークラスの初期化子に委譲しなければいけない。もし、継承プロパティに値を割り当てた後に初期化の委譲が出来ると仮定すると、継承プロパティの値はスーパークラス自身のdesignated initializerによって上書きされてしまう。

    上述のように、Swiftは先ず「プロパティを保有しているクラスで初期化する」ことを優先します。したがって、継承したプロパティにサブクラスで値を割り当てることは「プロパティのカスタマイズ」になります。そういう観点で2段階初期化を見てみると、「上への委譲」であるsuper.init()が、1段階目と2段階目の初期化の境目になっているということが分かります。

    2つ目の文章

    もし、継承プロパティに値を割り当てた後に初期化の委譲が出来ると仮定すると、継承プロパティの値はスーパークラス自身のdesignated initializerによって上書きされてしまう。

    は公式マニュアルにある以下の文章の意訳です。これは「もしこういうことが出来ると仮定すると、こうなる」という話で、実際はコンパイルエラーになるので実行はできません。

    “If it doesn’t, the new value the designated initializer assigns will be overwritten by the superclass as part of its own initialization.”

    抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

    具体的に見ると分かると思いますので、先程作ったサブクラスの1番目のdesignated initializerを少し改良してみます。

    // 委譲後に継承プロパティを変更。これは可能
    init(property: Int) {
        super.init(property:property, name:"subclass")
        name = "subclass by designated initializer"
    }
    

    上の「safety check 2」を満たすように書けば、このようになります。順序としては、「上に委譲」から「継承プロパティの変更」になります。上記2つ目の文章をコードに落とすと、

    // 継承プロパティを変更してから委譲。コンパイルエラー
    init(property: Int) {
        name = "subclass by designated initializer"
        super.init(property:property, name:"subclass")
    }
    //コンパイルエラー
    //error: use of 'self' in property access 'name' before super.init initializes self
    

    このようになります。継承したプロパティが初期化される(メモリを割り当てられる)のは、super.init()が呼び出された時です。したがって、それよりも前に値を与えることは出来ませんので、コンパイルエラーになります。

    Safety check 3:

    Convenience initializerは、プロパティに値を割り当てる前に(自身が保有するプロパティも含む)、別の初期化子に初期化を委譲しないといけない。もし、プロパティに値を割り当てた後に初期化の委譲が出来ると仮定すると、プロパティの値は自身の持つdesignated initializerによって上書きされてしまう。

    Convenience initializerはあくまで「カスタマイズ」が目的で、初期化をするのはdesignated initializerです。したがって、まず「(横に)委譲」するのが最優先です。2番目の文章は、safety check 2の場合と同様です。

    Safety check 4

    初期化子は、1段階目の初期化が完了して始めて、以下の項目を実行することが可能

    クラスインスタンスは、1段階目の初期化が完了するまでは有効ではありません。プロパティやメソッドにアクセス出来るようになるのは、1段階目の初期化が終わった後、クラスインスタンスが有効になった時点です。これは、safty check 2でソースコードを使って説明した具体例を見て頂くと分かると思います。

    2段階初期化の具体的な流れ

    以下、上で説明した4つの安全確認に基づいた2段階初期化が、具体的にどのように実行されるのかを見てみます。

    [図解]2段階初期化

    1段階目の流れ

    サブクラスインスタンスをconvenience initializerで作った場合を想定します。

    let subClass = SubClass()

    Convenience initializerは「横に委譲」していますので、初期化はサブクラスのdesignated initializerに委譲されます。

    // 横に委譲
    convenience init() {
        self.init(name: "subclass convenience")
    }
    

    今、サブクラスはプロパティを持っていませんので、サブクラスのdesignated initializerは初期化するプロパティがありません。したがって、designated initializerの仕事は「上に委譲」するだけです。

    // 初期化するプロパティがないので、上に委譲するだけ
    init(name: String) {
        super.init(property:1, name:name)
    }
    

    1段階目の最後は、基底クラス(base class)のdesignated initializerです。基底クラスは委譲先がありませんので、クラスの保有する格納プロパティを初期化するだけです。

    init(property: Int, name: String) {
        self.property = property
        self.name = name
    }
    

    ここまでで、サブクラスも含めた全てのプロパティが初期化されているはずです。もし初期化されていないプロパティがあれば、Swiftのコンパイラがエラーを出します。

    2段階目の流れ

    2段階目は「プロパティのカスタマイズ」ですから、今まで辿ってきた道を逆戻りします。先ず、基底クラス(スーパークラス)のdesignated initializerから出発して、カスタマイズ出来るプロパティがあれば実行します。次は、サブクラスのdesignated initializerで、ここでもカスタマイズするプロパティがあれば、変更します(今回の例ではなし)。最後は、元々クラスインスタンスを生成する際に使用したconvenience initializerです。

    なんとなく流れが分かったでしょうか?公式マニュアルに載っている絵も非常に分かりやすいので、そちらも参考下さい。

    まとめ

    • クラスは2つ初期化子を持つ。Designated InitializerとConvenience Initializer
    • Designated initializerは必須で、サブクラスでは「上に委譲」(super.init())しなければならない
    • Convenience initializerは任意。ルールは「横に委譲」、(1)同じクラスの別の初期化子を呼び出すこと、かつ(2)最後は必ずdesignated initializerを呼びだすこと
    • クラスは2段階で初期化される。1段階目で全てのプロパティを初期化、2段階目で値のカスタマイズをする