Swiftの初期化 | 初期化子の継承とオーバーライド

Swiftの初期化 | 初期化子の継承とオーバーライド

初期化子もクラスが持つ機能の1つですから、当然継承したりオーバーライド出来ます。クラスは2つ初期化子があって、その継承ルールもちょっと複雑です。

ここでは、主に初期化子のオーバーライドに関して、具体的な例を使って「どういう場合にオーバーライドするべきで、どういう場合にするべきではないか?」という点に関して詳しく説明していきます。また、Automatic Initializer Inheritanceという初期化子が自動的に継承される機能についても、具体例を使って詳しく説明します。

初期化子の継承とオーバーライド

Swiftではデフォルトで初期化子を継承しない

Object-Cと違って、Swiftのサブクラス(subclass)はデフォルトでスーパークラスの初期化子(initializer)を継承しません。このような仕様の場合、「毎回サブクラスで初期化子を定義しないといけないから面倒だ」というのは欠点になります。利点は「安全性が高まること」でしょうか?例えば、「単純な初期化子の継承をした場合にサブクラスのプロパティを初期化し忘れる」という状況を回避できます。

後で説明しますが、ある特定の状況では、初期化子が自動的に継承されるケースがあります。それをAutomatic Initializer Inheritanceと呼びます。敢えて和訳すると、「自動的な初期化子の継承」でしょうか?そのままですね。

サブクラスにおいて、スーパークラス(superclass)と同じ初期化子(パラメータが同じという意味です)を導入して、実装部分だけを書き換えたいケースがあるかもしれません。これはまさにメソッドで見たオーバーライド(overriding)です。重要なのは

スーパークラスのdesignated initializerと同じパラメータを持った初期化子をサブクラスで定義した場合、サブクラスでは必ずオーバーライドoverrideキーワードを使う

ということです。以下で少し詳しく見ていきます。

先程もちらっと述べましたが、オーバーライドで肝になるのは「外見」、つまり初期化子が持つパラメータです。以降「同じ初期化子」という場合はパラメータの数、型が同じ初期化子という意味で使います。

スーパークラスのdesignated initializerならオーバーライド

スーパークラスのdesignated initializerと「同じ」サブクラスの初期化子を定義した場合、実質的にはスーパークラスのdesignated initializerをオーバーライドしたことになります。したがって、この場合は、サブクラスの初期化子の定義にoverrideキーワードが必要になります。これは、デフォルト初期化子をオーバーライドした場合にも適用されます。

これは私自身の解釈ですが、designated initializerは異なるクラス間の委譲を想定しているため、必ずオーバーライドさせるような仕様になっているのだと思います。

overrideキーワードを指定することによって、本当に「同じ」初期化子かどうか、スーパークラス側とサブクラス側でチェックを行います。これは、プロパティ、メソッド、またはサブスクリプトをオーバーライドする場合と同様です(最も近いのはメソッドでしょうか)。

公式マニュアルに注意書きがありますが、overrideキーワードは、例えサブクラスのconvenience initializerがスーパークラスのdesignated initializerをオーバーライドした場合でも必要です。オーバーライドの際に重要なのは、元々ある初期化子がdesignated initializerであるかどうか、です。

“NOTE

You always write the override modifier when overriding a superclass designated initializer, even if your subclass’s implementation of the initializer is a convenience initializer.”

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

試してみると、

// 初期化子のオーバーライド
class SuperClass {
    var property = "this is superclass"
}

class SubClass: SuperClass {
    override init() {
        super.init()
        property = "this is subclass"
    }
}

let subClass = SubClass()
print(subClass.property)
//"this is subclass"と表示

このような感じです。

先ず、スーパークラスでは全ての格納プロパティ(stored property)(今は1つのみ)に初期値を与えているので、デフォルト初期化子init()が作られます。デフォルト初期化子はdesignated initializerなので、サブクラスで「同じ」初期化子を作る場合はoverrideキーワードが必要です。

サブクラスの初期化子の中身は、上に委譲、そしてスーパークラスのプロパティの変更、という順番です。これはクラスの2段階初期化で見た通りです。

正確に書くと、サブクラスの初期化子内部ではsuper.propertyとするのが良いかもしれません。今はプロパティが1つしかないので自明ですが、プロパティが増えてきた場合には明確に区別しておくと可読性が上がるかもしれません。

もしoverrideを外すとコンパイルエラーになります。

// overrideなしだとエラー
class SubClass: SuperClass {
    init() {
        ....
}
//error: overriding declaration requires an 'override' keyword

スーパークラスのconvenience initializerならオーバーライドじゃない

逆に、サブクラスの初期化子がスーパークラスのconvenience initializerと「同じ」ケースを考えます。Swiftでは、スーパークラスのconvenience initializerがサブクラスによって直接呼び出されることは絶対ありません。これは、クラスの初期化子の委譲で説明した3つのルールに照らして考えるとすぐに分かります。

したがって、このケースでは、サブクラスはスーパークラスの初期化子のオーバーライドを提供している訳ではない(ということになる)ので、overrideキーワードは不要です。これも具体的に試してみると、

// スーパークラスのconvenience initializerと同じ初期化子
class SuperClass {
    var property = "this is superclass"
    convenience init(parameter: String) {
        self.init()
        property = parameter
    }
}

class SubClass: SuperClass {
    init(parameter: String) {
        super.init()
        property = parameter
    }
}

let subClass = SubClass(parameter: "this is subclass")
print(subClass.property)
//"this is subclass"と表示

このようになります。

初期化子の中身ではプロパティをセットしているだけなので、余り良い例ではありませんが、「同じ」初期化子をスーパークラスとサブクラスで定義しているにも関わらず、overrideキーワードがありません。これは、スーパークラスのconvenience initializerと「同じ」初期化子をサブクラスで定義したためです。図で見ると分かりやすいと思いますが、

[図解]convenience initializerと同じサブクラスの初期化子

こんな感じです。

スーパークラスのconvenience initializerは、当然同じクラス上のdesignated initializerに初期化を委譲します。一方、サブクラスのdesignated initializerは、上に委譲しています。今パラメータだけ見ると、スーパークラスのconvenience initializerとサブクラスのdesignated initializerは一緒です。しかし、この図から一発で分かると思いますが、

サブクラスのdesignated initializerがスーパークラスのconvenience initializerを経由(委譲)することは絶対にありません。

スーパークラスのconvenience initializerへサブクラスから委譲しようとすると、コンパイルエラーになります。

Automatic Initializer Inheritance

初期化子が継承される条件とは?

最初に述べたように、デフォルトでサブクラスはスーパークラスの初期化子を継承しません。しかし、ある一定の条件を満たすと、Swiftは自動的にスーパークラスの初期化子を継承します。これをAutomatic Initializer Inheritanceと呼びます。

自動的に初期化子が継承される条件というのは以下の通りです。ただし、サブクラスの全てのプロパティがデフォルト初期値を持っていることが必要になります。

ルール1
サブクラスがdesignated initializerを定義していない場合、そのサブクラスは自動的にスーパークラスの全てのdesignated initializerを継承する

ルール2
サブクラスがスーパークラスのdesignated initializerを「全て」実装している場合(ルール1に従って継承するか、またはサブクラス独自の実装)、そのサブクラスは自動的にスーパークラスの全てのconvenience initializerを継承する

この条件から分かるように、ルール1を満たすと自動的にルール2も満たします。従って、ルール1を満たすと全ての初期化子が(designatedもconvenienceも)継承されることになります。

初期化子の継承を具体例で理解する

今紹介した初期化の継承を具体例で見ていきます。公式マニュアルと全く同じ構造になってしまいました。

// 初期化の継承
class Song {
    var title: String

    init(title: String) {
        self.title = title
    }

    convenience init() {
        self.init(title: "[タイトル無し]")
    }
}

let someSong = Song(title: "歌")
print(someSong.title)
//"歌"

let nonamedSong = Song()
print(nonamedSong.title)
//"[タイトル無し]"

class Single: Song {
    var price: Int

    init(title: String, price: Int) {
        self.price = price
        super.init(title: title)
    }

    override convenience init(title: String) {
        self.init(title: title, price: 200)
    }
}

class Album: Single {
    var description: String {
        return "タイトル:\(title)、価格:\(price)円"
    }
}

var someAlbum = [
    Album(),
    Album(title:"2曲目", price:300),
    Album(title:"3曲目")
]

for single in someAlbum {
    print(single.description)
}
//タイトル:[タイトル無し]、価格:200円
//タイトル:2曲目、価格:300円
//タイトル:3曲目、価格:200円

順番に説明します。

基底クラスSong | 初期化子は2つ

基底クラスでは「歌」を管理するクラスSongです。今、曲のタイトルのみを格納プロパティ(文字列型)で保有しています。

// 歌クラス
class Song {
    var title: String
    ....
}

Designated initializerはパラメータを1つ持ち、プロパティを初期化しています。プロパティが1つしかないので、これは自明ですね。

init(title: String) {
    self.title = title
}

Convenience initializerはパラメータを持たない初期化子になっています。こいつは、designated initializerを呼びださなければいけません(委譲のルール2と3)。

convenience init() {
    self.init(title: "[タイトル無し]")
}

パラメータが無いので、直接タイトルに文字列を与えています。公式マニュアルにもあるような、委譲関係を示した絵を入れると、

[図解]基底クラスの初期化子の委譲

という感じになります(公式と構造は同じですね)。実際にクラスインスタンスを作ってテストしてみると、初期化子はちゃんと機能していることが分かります。

let someSong = Song(title: "歌")
print(someSong.title)
//"歌"

let nonamedSong = Song()
print(nonamedSong.title)
//"[タイトル無し]"

サブクラスSingle | 初期化子のオーバーライド

歌クラスを継承したサブクラスSingleです。ここで歌から「シングル(アルバム)」という物になった(ことにした)ので、価格(price)が追加されています。

// サブクラスのプロパティ
class Single: Song {
    var price: Int
    ....
}

パラメータが2つないと、このサブクラスの持つ全てのプロパティに外から値を代入することが出来ませんので、designated initializerは以下のようになります。

init(title: String, price: Int) {
    self.price = price
    super.init(title: title)
}

クラス自身が持つプロパティ(price)を初期化してから上に委譲しています(1段階目の初期化)。

ここで、サブクラスSingleは、もう1つ初期化子を持っています。

override convenience init(title: String) {
    self.init(title: title, price: 200)
}

ややこしい感じがしますが、順序立てて考えるとそれほど難しくありません。考える順番は様々ですが、ここでの流れとしては「同じクラス → 継承元」という初期化の流れと同じように考えてみます。

この初期化子では、「パラメータを1つ(title: String)与え、priceは内部で初期化したい」、とします。そうすると、初期化子が取り得る形は

init(title: String) {
    ....
}

です。

今、サブクラスSingleは既にdesignated initializerを持っています。こいつは、クラスの持つ全てのプロパティを初期化できますので、改めてdesignated initializerを作る必要はありません。ですから、convenienceキーワードを付けて、designated initializerを再利用します。そうすると、

convenience init(title: String) {
    self.init(....) // designated initializerへ委譲(横に委譲)
}

となります。

このクラスが基底クラスなら、ここで終わりですが、サブクラスですので継承元を見てみます。そうすると、この形(文字列のパラメータが1つ)は基底クラスのdesignated initializerと「同じ」です。初期化子のパラメータがスーパークラスのdesignated initializerと同じ場合はオーバーライドするというルールですから、overrideキーワードが必要になって、

override convenience init(title: String) {
    self.init(....) // designated initializerへ委譲(横に委譲)
}

という具合に、最初に見た構文になりました。

さらに、automatic initializer inheritanceのルール2(スーパークラスが持つ全てのdesignated initializerを定義)から、スーパークラスの持つ残りの初期化子が継承されます。これを絵にしてみると、

[図解]サブクラスの初期化子の委譲(Singleクラス)

という感じになって、スーパークラスSongのconvenience initializerが自動的に継承されます。

サブクラスSingleでは、スーパークラスのdesignated initializerをオーバーライドした初期化子がconvenience initializerになっていますが、これは初期化子継承のルールとは関係ありません。あくまで、designated initializerと同じパラメータを持つ初期化子が、サブクラスで作られたかどうか、が初期化子継承の判定基準になります。

継承された初期化子の中身はスーパークラスと全く一緒です。つまり、

convenience init() {
    self.init(title: "[タイトル無し]")
}

という初期化子が、サブクラスで定義されていることになります。こいつは同じサブクラスのconvenience initializerであるinit(title: String)を呼び出します。

本当に初期化子が継承されているかは、クラスインスタンスを作ってみると分かります。

let noSingle = Single()
let expensiveSingle = Single(title: "高価な歌", price: 10000)
let someSingle = Single(title: "シングルアルバム")

サブクラスAlbum | 初期化子の継承

最後に、サブクラスSingleをさらに継承したAlbumを作りました。

class Album: Single {
    var description: String {
        return "タイトル:\(title)、価格:\(price)円"
    }
}

var someAlbum = [
    Album(),
    Album(title:"2曲目", price:300),
    Album(title:"3曲目")
]

for single in someAlbum {
    print(single.description)
}
//タイトル:[タイトル無し]、価格:200円
//タイトル:2曲目、価格:300円
//タイトル:3曲目、価格:200円

このクラスは、計算プロパティを追加しただけで、初期化子がありません。これは、automatic initializer inheritanceのルール1に該当するので、サブクラスSingleの持つ初期化子が全て継承されます。

[図解]サブクラスの初期化の継承

上のコードで、実際に継承された初期化子を使ってインスタンスを生成していますが、ちゃんと動いているのが分かります。

まとめ

  • クラスの初期化子はデフォルトでは継承されない
  • スーパークラスのdesignated initializerと同じパラメータを持つ初期化子を定義した場合、必ずoverrideキーワードを付ける
  • スーパークラスのconvenience initializerと同じパラメータを持つ初期化子を定義した場合、overrideキーワードは必要ない
  • 初期化子の継承には2つルールがある(Automatic Initializer Inheritance)。その前提として、サブクラスの全てのプロパティが初期値を持っている必要がある
  • Designated initializer継承の条件: サブクラスでdesignated initializerを定義していないこと
  • Convenience initializer継承の条件: サブクラスがスーパークラスのdesignated initializerを全て実装していること