iOS
Swift

【Swift】イニシャライザのしくみ

Classのイニシャライザ

Classのイニシャライザには designated initializerconvenience initializer の2種類が存在します。
それぞれのイニシャライザは以下のルールに則る必要があります。

  1. designated initializer は直近のスーパークラスの designated initializer を必ず呼び出さなければならない
  2. convenience initializer は必ず同じクラスの他の initializer を呼び出さなければならない
  3. convenience initializer は最終的には designated initializer を呼び出していなければならない

例を以下の図に示します。

image.png

この例では、スーパークラスは1つの designated initializer と2つの convenience initializer を持ちます。

convenience initializer の1つはもう1つの convenience initializer を呼び出し、そこで designated initializer を呼び出します。
これは上記のルール2と3を満たしています。

サブクラスは2つの designated initializer と1つの convenience initializer を持ちます。

convenience initializer は必ず同じクラスの他のinitializerを呼び出さなければならないので、2つの designated initializer の内1つを呼び出します。
これは上記のルール2と3を満たしています。

そして2つの designated initializer はルール1を満たすために、どちらもスーパークラスの designated initializer を呼び出します。

ここでそれぞれの initializer の定義方法を以下に示します。

// designated initializer
init(引数...) {
 処理...
 }

// convenience initializer
convenience init(引数...) {
 処理...
 }

Two-Phase Initializer

Swiftのクラスの初期化には2段階のフェイズが存在します。

フェイズ1では、クラスで定義した 保持型プロパティ(stored property) はすべてそのクラスの初期化のときには値が割り当てられている必要があります。
すべてのプロパティが初期値を持つことが確認できればフェイズ2に入り、インスタンスの値の変更が可能になります。

この2段階のフェイズにより、Swiftにおけるクラスの初期化は非常に安全なものとなります。

Swiftのコンパイラはこの要件を満たすために以下の4つの安全確認を行います。

  1. designated initializer はスーパークラスの designated initializer を呼び出す前にクラス内のすべての保持型プロパティを初期化する
  2. 継承したプロパティを更新する前にスーパークラスの designated initializer を呼び出さなければならない (そうでなければ、どれだけ継承したプロパティを書き換えてもスーパークラスの初期化で上書きされてしまう)
  3. convenience initializer では他の initializer を呼び出してからでなくてはプロパティの値を書き換えてはいけない (そうでなければ、プロパティの値を書き換えても他の initializer に上書きされる可能性がある)
  4. initializer は同じクラスのプロパティすべての値が初期化されるまで、どのメソッドを呼び出すことも、プロパティの読み込みも、selfの参照もできない

ここで改めて2つのフェイズの役割を上記の4つのルールに則って示します。

フェイズ1

  • クラスの initializer が呼び出される
  • 初期化はまだしないが新しいインスタンスのためのメモリを確保する
  • designated initializer がそのクラスで定義したすべての保持型プロパティを初期化することを確認する (メモリはここで初期化される)
  • スーパークラスの designated initializer を呼び出して同じことを行う
  • スーパークラスの頂点に達するまでこれを繰り返す
  • 頂点まで達し、すべての保持型プロパティが値を持つことを確認できたらインスタンスのメモリは完全に初期化されたとみなし、フェイズ1を終了する

フェイズ2

  • 頂点のクラスからサブクラスへ下っていく
  • それぞれのクラスの designated initializer にて、selfを参照してプロパティの書き換えやメソッドの呼び出しが可能となる
  • 最終的にそれぞれのクラスの convenience initializer も同様にインスタンスの値にアクセスできる

具体的なイメージを持つために、以下の図を元にフェイズ1の流れを見ていきます。

image.png

この例ではサブクラスの convenience initializer から初期化が始まり、同じクラスの designated initializer を呼び出します。
この段階では当然どのプロパティの値も変更できません。

そしてサブクラスの designated initializer はすべてのプロパティが値を持つことを確認し、スーパークラスの designated initializer を呼び出します。

スーパークラスの designated initializer も同様にすべてのプロパティが値を持つことを確認します。
これ以上スーパークラスは存在しないため、ここでメモリは初期化されてフェイズ1は完了です。

それでは次にフェイズ2を見ていきます。

image.png

まず、スーパークラスの designated initializer からインスタンスの値の変更などが可能になります。

そして、スーパークラスの designated initializer が処理を終了すればサブクラスの designated initializer もインスタンスの値へのアクセスが可能になります。

最後に、サブクラスの designated initializer が終了すれば、エントリーポイントである convenience initializer もインスタンスの値へのアクセスが可能になります。

参考

Apple Inc. “The Swift Programming Language" -Initialization