2020/1/12 Swift イニシャライザ(Initialization)工事中🏗

初期化(Initialization)は、クラス・構造体・列挙型のインスタンスを準備するプロセスです。このプロセスはインスタンスのストアドプロパティの初期化と、その他のインスタンスを使用するに際必要なセットアップや初期化が含まれます。

イニシャライザを定義することで初期化処理を実装します。イニシャライザは特定の型のインスタンスが生成される際呼び出される特別なメソッドです。Swiftのイニシャライザは値を返しません。イニシャライザの一番の役割は、インスタンスが初めて使われる時に正常に初期化されていることを確実にすることです。

クラス型のインスタンスはデイニシャライザを実装することもできます。デイニシャライザはクラスのインスタンスが削除(割当解除)される直前に独自のクリーンアップをします。

ストアドプロパティの初期化

クラスと構造体はインスタンスが生成される時までに、全てのストアドプロパティが適切な値で初期化されていなければなりません。ストアドプロパティの値が不確定のままの状態は許されません。

ストアドプロパティの初期化方法としては、イニシャライザによる初期化、あるいはストアドプロパティの宣言時にデフォルト値を代入する方法があります。

NOTE

イニシャライザによる初期化時、あるいは宣言時のデフォルト値の代入時は、プロパティオブザーバが呼ばれることはありません。

イニシャライザ

イニシャライザは、特定の方のインスタンス生成時に呼び出されます。一番基本的なイニシャライザの定義は以下のようにインスタンスメソッドのように定義します。

sample1-1

init() {
    // perform some initialization here
}

sample1-2ではFahrenheitで表現される温度を格納するFahrenheit構造体を定義しています。一つのDouble型のストアドプロパティtemperatureを持っています。

sample1-2

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

この構造体のイニシャライザinitは、引数を持たず、temperatureプロパティを32.0で初期化します。(32°F=0℃)

プロパティのデフォルト値

上記ではイニシャライザによりプロパティの初期化をしていますが、その代わりにプロパティの宣言時にデフォルト値を代入することができます。

プロパティ宣言時にデフォルト値を代入することでFahrenheit構造体をよりシンプルに定義することができます。

sample1-3

struct Fahrenheit {
    var temperature = 32.0
}

イニシャライザのカスタマイズ

 

イニシャライザの引数

イニシャライザの定義として、イニシャライザに引数を用意することができます。引数名と引数の型を定義します。

sample2-1ではCelsiusと言う名の構造体を定義しています。単位℃の温度を表現します。この構造体は二つのイニシャライザ

init(fromFahrenheit:)

init(fromKelvin:)

を実装しています。このイニシャライザは、他の単位(Kと℉)で与えられた温度を℃に変換してtemperatureInCelsiusプロパティを初期化します。

sample2-1

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

 

一番目のイニシャライザは、引数が一つで、アーギュメントラベルがfromFahrenheit、パラメーターネームがfahrenheitです。

二つ目のイニシャライザは、引数一つで、アーギュメントラベルがfromKelvin、パラメーターネームがkelvinです。どちらのイニシャライザも受け取った値を℃の値に変換してtemperatuerInCelsiusプロパティにセットします。

パラメーターネームとアーギュメントラベル

関数やメソッドと同様に、イニシャライザもパラメータネームとアーギュメントラベルを持つことができます。パラメータネームはイニシャライザの本体内で使われ、アーキュメントラベルはイニシャライザ呼び出し時に使われます。

しかしイニシャライザは、関数や引数のように、識別するための関数名は持ちません(全てイニシャライザ名はinit)。にも関わらず複数のイニシャライザを定義できる、ということなので、どのイニシャライザを呼び出すか特定するために、イニシャライザの引数の名前と型が重要になります。そのためSwiftではイニシャライザの中の全ての引数に(開発者が)アーキュメントラベルを定義しないと、Swiftが自動的にアーギュメントラベルを与えます。

下のsample2-2では、Color構造体を定義します。三つの定数プロパティred,green,blueを持ちます。これらのプロパティは0.0から1.0の間のそれぞれの色の値を格納します。

sample2-2

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

print(magenta.red,magenta.green,magenta.blue)
print(halfGray.red,halfGray.green,halfGray.blue)

//1.0 0.0 1.0
//0.5 0.5 0.5

Color構造体はred,green,blueの三つの要素のDouble型のネームドパラメータを持つイニシャライザを持ちます。またColor構造体は、whiteという一つの引数を持ち、三つの色の要素へ同じ値をセットするイニシャライザを持ちます。

両方のイニシャライザは新しいColorインスタンスを生成するために使われます。両方ともネームドパラメータに値を渡してインスタンスを生成します。

注意してほしいことは、これらのイニシャライザを呼び出すときに、「アーギュメントラベルを使用せずに呼び出す」ということはできない、ということです。アーギュメントラベルを定義した場合は必ずアーギュメントラベルを使用しなければなりません。省略するとコンパイルエラーが発生します。

アーギュメントラベルを省略したイニシャライザ呼び出し

イニシャライザ呼び出し時にアーギュメントラベルを使用したくない場合は、明示的にアーギュメントラベルを記述する代わりにアンダースコア( _ )を記述してください。

sample2-3は、sample2-1のCelsiusのサンプルの拡張版です。℃単位の数値を引数として受け取りCelsiusインスタンスを生成するイニシャライザを追加しています。

sample2-3

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}
let bodyTemperature = Celsius(37.0)
print(bodyTemperature.temperatureInCelsius)
// 37.0

三つ目のイニシャライザの定義により、Celsius(37.0)の呼び出しは、アーギュメントラベルを使用しなくても意図が明確です。アーギュメントラベル無しでイニシャライザを呼び出すために、

init( _ celsius : Double)

と定義しています。

オプショナル型のプロパティ

工事中🏗

 

初期化中に定数プロパティを割り当てる

イニシャライザ終了までに特定の値がセットされるのであれば、イニシャライザ中の任意のタイミングで定数プロパティに値をセットすることができます。定数プロパティに値が割り当てられてイニシャライザが終了すると、それ以上変更することはできません。

NOTE

クラスインスタンスの場合、定数プロパティは、それを導入するクラスによってのみ初期化中に変更できます。サブクラスでは変更できません。

sample2-4

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

SurveyQuestionインスタンスが生成された後はtextプロパティの値が変更されないことを実現するため、SurveyQuestionクラスのtextプロパティは定数プロパティです。textプロパティは定数ですが、イニシャライザ内ではセットすることができます。

デフォルトイニシャライザ

  1. 自分が持つ全てのプロパティにデフォルト値があること
  2. 独自のイニシャライザを一つも定義していないこと

→自分でイニシャライザを定義した場合はデフォルトイニシャライザは用意されない、ということ。 

上記を満たすクラス・構造体にはSwiftは自動的にデフォルトイニシャライザを用意します。デフォルトイニシャライザはシンプルに全てのプロパティにデフォルト値をセットした新しいインスタンスを生成します。

sample3-1

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

sample3-1ではShoppingListItemクラスを定義しています。ショッピングリストの商品名、数量、購入済みかどうか、をカプセル化したクラスです。

ShoppingListItemクラスは全てのプロパティがデフォルト値を持っています。そして親クラスはありません。ですのでShoppingListItemクラスは自動的にデフォルトイニシャライザが用意されます。nameプロパティはオプショナルString型なので自動的にデフォルト値はnilとなります。なのでデフォルト値はある、という扱い。

sample3-1では

ShoppingListItem()

と記述することでデフォルトイニシャライザを使用し、新しいインスタンスを生成しています。そしてそのインスタンスを変数itemに代入しています。

構造体型のメンバワイズイニシャライザ

  1. 独自のイニシャライザを定義していない

→自分でイニシャライザを定義した場合はデフォルトイニシャライザは用意されない、ということ。 

上記の条件を満たす構造体には自動的にメンバワイズイニシャライザが用意されます。デフォルトイニシャライザと違い、デフォルト値のないストアドプロパティがあっても、上記の条件を満たしていればメンバワイズイニシャライザが用意されます。

メンバワイズイニシャライザは新しい構造体インスタンスのメンバープロパティを初期化するための簡略記法です。名前付き引数に値を渡して、初期化します。

sample4-1

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"

sample4-1は、width,heightの二つのプロパティを持つSize構造体を定義しています。width,heightはデフォルト値として0.0を代入されているので、Double型として型推定されます。

Size構造体は独自のイニシャライザが定義されていませんので、自動的にメンバワイズイニシャライザが用意されます。ですので、メンバワイズイニシャライザを使用してインスタンスを生成できます。

メンバワイズイニシャライザを呼び出す時、デフォルト値があるプロパティに関しては引数を省略できます。sample4-1ではwidth,heightどちらもデフォルト値を持っています。メンバワイズイニシャライザ呼び出し時、width,heightのどちらか、あるいは両方を省略することができ、省略したものはデフォルト値を用いて初期化が行われます。

値型のイニシャライザデリゲーション

 

sample5-1

struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
print(centerRect.origin)
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

 

sample5-1では、長方形を表すRect構造体を定義しています。Rect構造体には二つの構造体SizeとPointが必要です。二つともデフォルト値0.0がそれぞれのプロパティに与えられています。

Rect構造体は三つの方法で初期化できます。

 

一つ目のRectイニシャライザ(4行目)、init()、は、独自のイニシャライザを定義しない場合に用意されるデフォルトイニシャライザと機能的には同じです。このイニシャライザの本体は空で、{}として記述されています。このイニシャライザを呼び出すと、origin,size共にデフォルト値Point(x:0.0,y:0.0),Size(width:0.0,height:0.0)で初期化されたRectインスタンスが返されます。

二つ目のRectイニシャライザ(5行目から)、init(origin:size:)、は、独自のイニシャライザを定義しない時に用意されるメンバワイズイニシャライザと機能的に同じです。このイニシャライザはシンプルに引数として受け取った値を適切に各プロパティにセットします。

三つ目のRectイニシャライザ(9行目から)、init(center:size:)、は、少し複雑です。引数として受け取ったcenter(中心点の座標)とsizeの値を基に、適切なorigin(起点の座標)を計算します。そして二つ目のイニシャライザinit(origin:size:)を呼び出して計算結果を渡します。結果的に計算結果として算出されたoriginがoriginプロパティにセットされたインスタンスが生成されます。

三つ目のイニシャライザは、計算結果をそのままoriginプロパティにセットすることもできますが、それよりもsample5-1のように、既にその機能を持っている二つ目のイニシャライザを利用した方がより便利で意図がわかりやすいということですね。

クラスの継承と初期化

スーパークラスから継承しているプロパティも含めて、クラスの全てのストアドプロパティは初期化時に初期値をセットする必要があります。

Swiftはクラス型の全てのストアドプロパティが適切に初期化されるために、二つのイニシャライザを用意しています。

  • 指定イニシャライザ(designated initializer)
  • コンビニエンスイニシャライザ(convenience initializer)

の二つです。

指定イニシャライザとコンビニエンスイニシャライザ

指定イニシャライザはそのクラスの主要なイニシャライザです。指定イニシャライザはそのクラスで導入された全てのプロパティを初期化し、初期化プロセスをスーパークラスにつなぐために適切なスーパークラスのイニシャライザを呼び出します。

クラスは指定イニシャライザを少数持つもので、一般的に指定イニシャライザは一つであることが普通です。

全てのクラスが少なくとも一つの指定イニシャライザを持たなくてはいけません。場合によってはその一つのイニシャライザは、一つ、あるいは複数のスーパークラスから継承した指定イニシャライザの場合もあります。

コンビニエンスイニシャライザは二次的な、クラスにとって補助的なイニシャライザです。工事中🏗

またクラスのインスタンスを特定のユースケースや特定の入力値の型を用いてインスタンスを生成するために、コンビニエンスイニシャライザを定義することができます。

クラスにとって必要なければ、コンビニエンスイニシャライザは作る必要はありません。一般的な初期化パターンを簡潔に書きたい場合、時間の節約、初期化の意図をはっきりさせたい場合にコンビニエンスイニシャライザを作りましょう。

指定イニシャライザとコンビニエンスイニシャライザの構文

クラスの指定イニシャライザの書き方は、値型のシンプルなイニシャライザの書き方と同じ方法です。

init(引数){
    //本体の文
}

コンビニエンスイニシャライザも同じスタイルで書きますが、convenience修飾子をinitキーワードの前に配置し、スペースで区切ります。

convenience init(引数){
    //本体の文
}

クラスタイプのイニシャライザデリゲーション

指定イニシャライザとコンビニエンスイニシャライザの関係性をシンプルにするため、Swiftではイニシャライザ間のデリゲーションコールに対して以下の三つのルールを適用します。

ルール1

指定イニシャライザは、直接のスーパークラスの指定イニシャライザを呼び出さないといけない

ルール2

コンビニエンスイニシャライザは同じクラスの別のイニシャライザを呼び出さないといけない

ルール3

コンビニエンスイニシャライザは最終的に指定イニシャライザを呼び出さないといけない

以上をまとめると

  • 指定イニシャライザは常にスーパークラスの指定イニシャライザを呼び出す。
  • コンビニエンスイニシャライザは常に同じクラスのイニシャライザを呼び出す。

図にすると以下のようになります。../_images/initializerDelegation01_2x.png

ここで、スーパークラスは一つの指定イニシャライザと二つのコンビニエンスイニシャライザを持っています。右側のコンビニエンスイニシャライザが真ん中のコンビニエンスイニシャライザを呼び出し、真ん中のコンビニエンスイニシャライザが左側の一つの指定イニシャライザを呼び出します。これは上のルール2とルール3を満たしています。上図ではスーパークラスのスーパークラスはありませんので、ルール1は当てはまりません。

上図のサブクラスは、二つの指定イニシャライザと一つのコンビニエンスイニシャライザを持っています。ルール2により、コンビニエンスイニシャライザは二つの指定イニシャライザのうちの一つを呼び出さないといけません。これは上のルール2とルール3を満たします。ルール1を満たすために、サブクラスの二つの指定イニシャライザは、スーパークラスの一つの指定イニシャライザを呼び出さないといけません。

NOTE

これらのルールは、クラスのユーザーが各クラスのインスタンスを作成する方法には影響しません。上の図のイニシャライザを使用して、それらが属するクラスの完全に初期化されたインスタンスを作成できます。ルールは、クラスのイニシャライザの実装を記述する方法にのみ影響します。

下の図はさらに複雑な四つのクラスのクラス階層です。この階層内の指定イニシャライザはクラス初期化の「ファンネル」ポイントとして機能しています。下の図は、指定イニシャライザがチェーン内のクラス間の相互関係を簡素化する方法を示しています。../_images/initializerDelegation02_2x.png

2段階初期化

Swiftでのクラスの初期化は二段階のプロセスで行われます。第一段階では、そのクラスにより導入された全てのストアドプロパティに初期値が割り当てられます。一度全てのストアドプロパティの初期値が決定されると、第二段階に進みます。第二段階では各クラスが使用される前にストアドプロパティの値をカスタマイズする機会があります。

二段階初期化により、クラス階層の中のそれぞれのクラスが完全な初期化の柔軟性を担保され、なおかつ初期化は安全に行われます。二段階初期化により、初期化前のプロパティの値へのアクセスは回避されます。二段階初期化により、他のイニシャライザによる予期せぬ違う値による初期化も回避されます。

Swiftのコンパイラはエラー無しで完全に二段階初期化を行うため、四つのsafty-checksを行います。

Safety check 1

あるクラスの指定イニシャライザが、そのクラスのスーパークラスの指定イニシャライザを呼び出す前に(デリゲートする前に)、そのクラスで導入された全てのプロパティを確実に初期化しているか。

上で説明したように、全てのストアドプロパティの初期値が確定した時に初めてオブシェクトのメモリが完全に初期化されたと認識されます。このルールを満たすために指定イニシャライザは、スーパークラスにデリゲートする前に、そのクラスの全てのプロパティを確実に初期化します。

Safety check 2

継承したプロパティに値を割り当てる前に、指定イニシャライザは、スーパークラスのイニシャライザを呼び出しているか。

このチェックポイントを満たさないと、指定イニシャライザにより割り当てられた新しい値が、スーパークラスの初期化により上書きされてしまう。

Safety check 3

同じクラスで定義されたプロパティを含めたあらゆるプロパティに値を割り当てる前に、コンビニエンスイニシャライザが別のイニシャライザを呼び出しているか。このチェックポイントを満たさないと、コンビニエンスイニシャライザにより割り当てられた新しい値が、同じクラスの指定イニシャライザにより上書きされてしまいます。

Safety check 4

イニシャライザはいかなるインスタンスメソッドも呼び出せない。イニシャライザはいかなるインスタンスプロパティの値にもアクセスできない。第一段階の初期化が完了するまでselfを参照できない。

クラスのインスタンスは第一段階が終わるまで正当なインスタンスと見なされません。第一段階初期化が完了して初めてクラスインスタンスは正当なインスタンスと見なされ、メソッドの呼び出しが可能になります。

この四つのチェックポイントに基づいて、どのように二段階初期化が行われるか見てみましょう。

Phase 1

  • クラスの指定イニシャライザ、あるいはコンビニエンスイニシャライザが呼び出される。
  • そのクラスの新しいインスタンスにメモリが割り当てられる。メモリはまだ初期化されていない。
  • そのクラスの指定イニシャライザが、そのクラスで導入された全てのストアドプロパティに初期値があるか確認する。確認後それらのストアドプロパティ用のメモリが初期化される。
  • スーパークラスのストアドプロパティについて同様の処理を行うため、指定イニシャライザはスーパークラスのイニシャライザに処理を託す(デリゲート)。
  • 上記の処理を連鎖の頂点に達するまで、クラスの継承の連鎖をさかのぼる。
  • 頂点に達し、最後のクラスが、そのクラスの全てのストアドプロパティに初期値があることを確認したら、そのインスタンスのメモリが完全に初期化されたと見なされ、第一段階が完了する。

Phase 2

  • 連鎖の頂点から戻り、それぞれの指定イニシャライザはさらにインスタンスをカスタマイズする機会を得る。この時点でイニシャライザはselfにアクセスでき、各自のプロパティを修正でき、各自のインスタンスメソッドを呼び出すことができます。
  • 最終的に、チェーン内のコンビニエンスイニシャライザがインスタンスをカスタマイズする機会を持ち、selfを扱えるようになります。

../_images/twoPhaseInitialization01_2x.png

上記のサンプル図で、サブクラスのコンビニエンスイニシャライザ(右側)の呼び出しから初期化がスタートします。このコンビニエンスイニシャライザはこの時点ではプロパティを修正することはできません。同じクラスの指定イニシャライザにデリゲートします。(同じクラスの指定イニシャライザを呼び出す。)

(サブクラスの)指定イニシャライザは、Safety check 1に従い、サブクラスの全てのプロパティに初期値があるかを確認します。その後、初期化の連鎖を上がるため、スーパークラスの指定イニシャライザを呼び出します。

スーパークラスの指定イニシャライザは、スーパークラスの全てのプロパティに初期値があるかを確認します。その上のスーパークラスは存在しないので、さらなるデリゲーションは必要ありません。

スーパークラスの全てのプロパティが初期値を持っていると確認されると、メモリが完全に初期化されたと見なされ、第一段階が完了します。

 


次に第二段階です。../_images/twoPhaseInitialization02_2x.png

スーパークラスの指定イニシャライザはインスタンスをカスタマイズする機会を得ます(上の図ではその必要はありませんが)。

スーパークラスの指定イニシャライザによるカスタマイズが終わると、サブクラスの指定イニシャライザによるカスタマイズが行われます(上の図ではその必要はありません)。

最終的に、サブクラスの指定イニシャライザによるカスタマイズが終わると、最初に呼び出されたコンビニエンスイニシャライザが追加のカスタマイズを行うことができます。

イニシャライザの継承とオーバーライド

Swiftはデフォルトでは、サブクラスはスーパークラスのイニシャライザを継承しません。Swiftのアプローチにより、「シンプルなスーパークラスのイニシャライザがサブクラスで複雑に継承され、それを使って完全に正しく初期化されていないサブクラスのインスタンスが生成される」状況は回避されます。

NOTE

スーパークラスのイニシャライザは安全で適切な状況で継承されることがあります。それについては後で説明します。

サブクラス内に、スーパークラスと同じイニシャライザを一つ、あるいは複数存在させたい場合、サブクラスにそれを実装することが可能です。

サブクラス内に、スーパークラスの指定イニシャライザと同じイニシャライザを用意する場合、その指定イニシャライザをオーバーライドします。ですので、サブクラスのイニシャライザ定義の前にoverride修飾子を記述する必要があります。これは自動的に用意されるデフォルトイニシャライザをオーバーライドする場合も同様です。

オーバーライドされたプロパティ・メソッド・サブスクリプトと同様に、override修飾子を書くことで、オーバーライドすべき指定イニシャライザがスーパークラスに存在するかをSwiftに確認させ、オーバーライドするイニシャライザの引数が(オーバーライドの)意図通りに指定されているかチェックします。

NOTE

スーパークラスの指定イニシャライザをオーバーライドする時は、それがサブクラス内でコンビニエンスイニシャライザであっても、override修飾子を書く必要があります。

逆にスーパークラスのコンビニエンスイニシャライザと同じイニシャライザをサブクラスに定義したい時はoverride修飾子は記述しない。

工事中🏗

下のサンプルはベースクラスのVehicleクラスを定義しています。Int型のデフォルト値0を持つストアドプロパティnumberOfWheelsを宣言しています。numberOfWheelsプロパティはコンピューテドプロパティdescription内で使用されます。descriptionはVehicleの特徴を文字列の説明文で表現します。

sample6-1

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

 

Vehicleクラスは唯一のストアドプロパティnumberOfWheelsに対して初期値0を持っています。そしてVehicleクラスは独自のイニシャライザが定義されていません。ですので、Vehicleクラスは自動的にデフォルトイニシャライザが用意されます。デフォルトイニシャライザがある場合、それは常にそのクラスの指定イニシャライザとなります。このデフォルトイニシャライザはnumberOfWheelsに0をセットして新しいVehicleクラスインスタンスを生成します。


次にVehicleクラスのサブクラスとしてBicycleクラスを定義しています。

sample6-2

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

 

サブクラスであるBicycleクラスには独自の指定イニシャライザinit()が定義されています。この指定イニシャライザはスーパークラス(Vehicle)の指定イニシャライザと同じなので、Bicycleクラスの指定イニシャライザにはoverride修飾子を付けます。

Bicycleクラスのinit()イニシャライザはsuper.init()で始まります。これはBicycleクラスのスーパークラスであるVehicleクラスのデフォルトイニシャライザを呼び出します。これにより、Bicycleがプロパティを修正する機会を持つ前に、継承したnumberOfWheelsプロパティがVehicleにより初期化されることが確実になります。super.init()の呼び出しの後、新しい値である2で、最初のnumberOfWheelsの値が置き換えられます。

Bicycleインスタンス生成後は、スーパークラス(Vehicle)から継承したdescriptionコンピューテドプロパティを呼び出し、numberOfWheelsプロパティの値が2に上書きされていることを確認できます。


もし初期化プロセスの第二段階で、サブクラスのイニシャライザのカスタマイズが何も無い場合、そしてスーパークラスが引数無しの指定イニシャライザを持っている場合、全てのサブクラスのストアドプロパティへの値の割り当ての後、super.init()の呼び出しを省略することができます。

次にVehicleの別のサブクラスHoverboardクラスを定義します。

sample6-3

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

Hoverboardクラスでイニシャライザは、自身で導入したcolorプロパティのみを初期化しています。初期化プロセスを完了させるのに明示的にsuper.init()を呼び出すのではなく、スーパークラスのイニシャライザの暗黙的呼び出しに頼っています(10〜13行目)。

Hoverboardクラスのインスタンスは、Vehicleクラスのイニシャライザにより提供されたnumberOfWheelsの初期値を使っています。

自動的なイニシャライザの継承

上記で言及した通り、基本的にサブクラスはスーパークラスのイニシャライザを継承しません。しかし、状況によってはスーパークラスのイニシャライザが自動的に継承されることがあります。実際、これは一般的なシナリオとしてイニシャライザのオーバーライドを記述する必要が無いということ、安全にできる場合は、最小限の労力でスーパークラスのイニシャライザを継承できることを意味しています。

サブクラスで導入した新しいプロパティに対してデフォルト値を提供することを想定した場合以下のルールが適用されます。

Rule 1

サブクラスに指定イニシャライザを定義していない場合、自動的にスーパークラスの指定イニシャライザが継承されます。

Rule 2

  • Rule 1により「スーパークラスの指定イニシャライザ」を自動的に継承した場合
  • サブクラスの定義の一部として「スーパークラスの指定イニシャライザ」を独自に実装している場合

上記のように全てのスーパークラスの指定イニシャライザの実装を、サブクラスが提供している場合、スーパークラスのコンビニエンスイニシャライザを自動的に継承されます。

実際の指定イニシャライザとコンビニエンスイニシャライザ

以下のサンプルで指定イニシャライザ、コンビニエンスイニシャライザ、自動的なイニシャライザの継承について見ていきます。このサンプルは

Food

RecipeIngredient

ShoppingListItem

の三つのクラスの階層を定義し、それらのイニシャライザの相互の働きについて見ていきます。

クラス階層のベースクラスであるFoodクラスを定義します。シンプルなクラスので、食料の名前をカプセル化します。FoodクラスはString型のnameプロパティを導入し、Foodクラスのインスタンスを生成する二つのイニシャライザを定義しています。

sample7-1

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

下の図はFoodクラスのイニシャライザの連鎖について示しています。../_images/initializersExample01_2x.png

クラスはメンバワイズイニシャライザがありません。Foodクラスにはnameという名前の引数を取る指定イニシャライザが定義されています。このイニシャライザは特定の名前を持つ新しいFood型のインスタンスを生成するために使われます。

Foodクラスのinit(name:String)イニシャライザは指定イニシャライザとして定義されています。なぜならこのイニシャライザによりFoodクラスのインスタンスの全てのストアドプロパティが完全に初期化されるからです。Foodクラスはさらなるスーパークラスを持っていません。ですのでinit(name:String)イニシャライザは初期化処理を完了させるためにsuper.init()を呼び出す必要はありません。

Foodクラスは引数無しのコンビニエンスイニシャライザ

init()

も持っています。init()イニシャライザはFoodクラスの指定イニシャライザinit(name:String)に引数”[Unnamed]”を渡して呼び出す(デリゲートする)ことで、デフォルトの仮の名前”[Unnamed]”を新しいFoodインスタンスに提供します。


二つ目のクラスはFoodクラスのサブクラスであるRecipeIngredientクラスです。このクラスはクッキングレシピの食材をモデルにしています。このクラスはInt型のプロパティquantity導入しています。さらにFoodクラスから継承したnameプロパティを持ちます。そしてRecipeIngredientクラスのインスタンスを生成する二つのイニシャライザを定義しています。

sample7-2

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

下の図はRecipeIngredientクラスのイニシャライザの連鎖を示しています。../_images/initializersExample02_2x.png

RecipeIngredientクラスは一つの指定イニシャライ

init(name:String,quantity:Int)

を持っています。新しいRecipeIngredientインスタンスの全てのプロパティに初期値をセットします。このイニシャライザは受け取った引数quantityの値をquantityプロパティに代入するところからスタートします。quantityプロパティはRecipeIngredientクラスで導入された唯一の新しいプロパティです。その後、Foodクラスのinit(name:String)イニシャライザを呼び出して(デリゲートして)います。このプロセスはsafety check 1を満たしています。

RecipeIngredientクラスはコンビニエンスイニシャライザinit(name:String)も定義しています。このイニシャライザは引数nameのみでRecipeIngredientインスタンスを生成するために使われます。このコンビニエンスイニシャライザは明示的な数量の指定をせずに、quantityが1のあらゆる種類のRecipeIngredientインスタンスを生成することを想定しています。このコンビニエンスイニシャライザにより、RecipeIngredientインスタンスの生成がより迅速で便利になり、数量1のインスタンスを生成する際のコードの重複を回避することができます。このコンビニエンスイニシャライザはシンプルに同じクラスの指定イニシャライザを引数quantityに1を渡して呼び出して(デリゲートして)います。

RecipeIngredientクラスのコンビニエンスイニシャライザinit(name:String)は、Foodクラスの指定イニシャライザinit(name:String)と同じ引数を取ります。このコンビニエンスイニシャライザはスーパークラスの指定イニシャライザをオーバーライドしていますので、override修飾子を付けなければいけません。

RecipeIngredientクラスはinit(name:String)イニシャライザをコンビニエンスイニシャライザとして提供しています。しかしそうであるとしても、それでもやはりRecipeIngredientクラスはスーパークラス(Food)の指定イニシャライザの実装を提供しています。それゆえに、RecipeIngredientクラスは自動的に全てのスーパークラスのコンビニエンスイニシャライザも継承します。

このサンプルでは、RecipeIngredientクラスのスーパークラスであるFoodクラスは、一つのコンビニエンスイニシャライザinit()を持っています。ですのでこのイニシャライザはRecipeIngredientクラスに継承されます。RecipeIngredientクラスが自動的に継承したinit()は、Foodバージョンのinit()とほぼ同じ挙動ですが、一つ違うのは、自動的に継承したinit()は、RecipeIngredientクラスのinit(name:String)にデリゲートする、という部分です。

自動継承したinit()が(RecipeIngredientのコンビニエンスイニシャライザの)init(name:String)にデリゲート。init(name:String)は(RecipeIngredientの指定イニシャライザの)init(name: String, quantity: Int)にデリゲート。デリゲートが2回為されている。

三つのイニシャライザがRecipeIngredientインスタンス生成に使われます。()


3つ目の、そして階層の最後のクラスはRecipeIngredientのサブクラスであるShoppingListItemです。

ショッピングリスト内の全てのアイテムは一番最初は未購入の状態です。この事実を表現するため、ShoppingListItemクラスはBool型のプロパティpurchased(デフォルト値false)を導入しています。それとコンピューテドプロパティdescriptionを導入しています。これはShoppingListItemインスタンスの文章の説明を返します。

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

NOTE

ShoppingListItemは自身で導入したプロパティにデフォルト値があるので、イニシャライザを定義していません。

ShoppingListItemクラスはイニシャライザを全く定義していません。ですのでShoppingListItemはスーパークラスから指定イニシャライザとコンビニエンスイニシャライザを自動的に継承します。

 

 

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID213

コメントを残す

メールアドレスが公開されることはありません。