見出し画像

Swiftでプログラミング- Initialization 2

Class Inheritance and Initialization

クラスがそのスーパークラスから継承するプロパティ、クラスのすべての保存プロパティは、初期化、初期値を割り当てられなければなりません。

Swiftは、すべての継承した保存プロパティが初期値を受け取ることを確実にするために、クラスタイプのための2種類のイニシャライザ、designated initializersとconvenience initializersを定義します。

Designated Initializers and Convenience Initializers

指定イニシャライザ(Designated Initializer)は、クラスの主要なイニシャライザです。指定イニシャライザは、そのクラスによって導入されるすべてのプロパティを全て初期化し、適切なスーパークラスのイニシャライザを呼び出して、スーパークラスの連鎖で初期化します。

すべてのクラスには、少なくとも1つの指定イニシャライザが必要です。場合によっては、後述の「イニシャライザの自動継承」で説明するように、1つ以上の指定イニシャライザをスーパークラスから継承することで、この要件を満たすことができます。

コンビニエンス・イニシャライザ(Convenience Initializer)は、クラスの補助的なイニシャライザです。コンビニエンス・イニシャライザを定義すると、コンビニエンス・イニシャライザと同じクラスの指定イニシャライザのパラメータの一部をデフォルト値に設定して呼び出すことができます。また、特定のユースケースや入力値のタイプに合わせて、そのクラスのインスタンスを作成するコンビニエンス・イニシャライザを定義することもできます。

クラスが必要としない場合は、コンビニエンス・イニシャライザを用意する必要はありません。初期化することで時間を節約したり、クラスの初期化の意図を明確にしたい場合は、コンビニエンス・イニシャライザを作成します。

Syntax for Designated and Convenience Initializers

指定イニシャライザは、通常のイニシャライザと同じ方法で記述されます。

   init(parameters) {
       statements
   }

コンビニエンス・イニシャライザは、同じスタイルで書かれますが、initキーワードの前に"convenience"を置き、スペースで区切っています。

    convenience init(parameters) {
       statements
   }

Initializer Delegation for Class Types

指定イニシャライザとコンビニエンス・イニシャライザの関係を単純化するために、Swiftはイニシャライザ間のデリゲーションコールに以下の3つのルールを適用します。

ルール1
指定イニシャライザは、その直属のスーパークラスから指定イニシャライザを呼び出さなければなりません。
ルール2
コンビニエンス・イニシャライザは、同じクラスから別のイニシャライザを呼び出さなければなりません。
ルール3
コンビニエンス・イニシャライザは、最終的に指定イニシャライザを呼び出さなければなりません。

これを簡単に覚えるには、次のようにします。

・指定されたイニシャライザは、常に上にデリゲートしなければなりません。
・コンビニエンス・イニシャライザは、常に横方向にデリゲートしなければなりません。

ここでは、スーパークラスには、1つの指定イニシャライザと2つのコンビニエンス・イニシャライザを考えます。1つのコンビニエンス・イニシャライザが別のコンビニエンス・イニシャライザを呼び出し、そのコンビニエンス・イニシャライザが1つの指定イニシャライザを呼び出します。これは上記のルール2と3を満たしています。スーパークラスは、それ自体がさらにスーパークラスを持たないので、ルール1は適用されません。

2つの指定イニシャライザと1つの簡易イニシャライザを持っているとコンビニエンス・イニシャライザは、2 つの指定されたイニシャライザのいずれかを呼び出さなければなりません。これは上述のルール2と3を満たしています。両方の指定イニシャライザは、上記のルール1を満たすために、スーパークラスの単一の指定イニシャライザを呼び出さなければなりません。

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

4つのクラスのより複雑なクラス階層で指定されたイニシャライザが、クラスの初期化のための「通過点」ポイントとして機能し、チェーン内のクラス間の相互関係を単純化できます。

Two-Phase Initialization

Swiftにおけるクラスの初期化は、2段階のプロセスです。最初の段階では、各保存されたプロパティは、それを導入したクラスによって初期値が割り当てられます。すべての保存プロパティの初期状態が決定されると、第2フェーズが開始され、各クラスには、新しいインスタンスが使用可能とみなされる前に、その保存されたプロパティをさらにカスタマイズする機会が与えられます。

2段階の初期化プロセスを使用することで、初期化の安全性を確保しつつ、クラス階層内の各クラスに完全な柔軟性を与えることができます。2段階の初期化により、初期化される前にプロパティ値にアクセスされることを防ぎ、また、初期化段階での不意なプロパティ値の変更を防ぎます。

Swiftの2段階の初期化プロセスは、Objective-Cの初期化と似ています。主な違いは、フェーズ1の間、Objective-Cでは、すべてのプロパティに0またはnullの値(0またはnilなど)を割り当てることです。Swiftの初期化フローは、カスタムの初期値を設定できるという点で、より柔軟性があり、0またはnilが有効なデフォルト値ではない型に対処することができます。

Swiftのコンパイラは、2段階の初期化がエラーなく完了することを確認するために、4つの有用な安全チェックを行います。

安全チェック1
指定されたイニシャライザは、スーパークラスのイニシャライザに委任する前に、そのクラスによって導入されたすべてのプロパティが初期化されていることを保証しなければなりません。
前述のように、オブジェクトのメモリは、保存されているすべてのプロパティの初期状態がわかって初めて、完全に初期化されたとみなされます。このルールを満たすためには、指定されたイニシャライザは、連鎖的に引き渡す前に、自身のすべてのプロパティが初期化されていることを確認しなければなりません。

安全チェック2
指定イニシャライザは、継承されたプロパティに値を割り当てる前に、スーパークラスのイニシャライザに委ねなければなりません。そうしないと、指定イニシャライザが割り当てた新しい値は、スーパークラスが自身の初期化の一部として上書きしてしまいます。

安全チェック3
コンビニエンス・イニシャライザは、任意のプロパティ(同じクラスで定義されたプロパティを含む)に値を割り当てる前に、他のイニシャライザにデリゲートしなければなりません。そうしないと、コンビニエンス イニシャライザが割り当てる新しい値は、自分のクラスの指定イニシャライザによって上書きされます。

安全チェック4
イニシャライザは、初期化の第一段階が完了するまで、インスタンスメソッドを呼び出したり、インスタンスプロパティの値を読み取ったり、selfを値として参照したりすることはできません。

第一段階が終了するまで、クラスのインスタンスは完全には有効ではありません。プロパティにアクセスしたり、メソッドを呼び出したりできるのは、第 1 フェーズが終了した時点でクラスのインスタンスが有効であることがわかってからです。

上記の 4 つの安全チェックに基づいて、2 段階の初期化がどのように行われるかを説明します。

第1フェーズ

指定またはコンビニエンス・イニシャライザがクラスに呼び出されます。
そのクラスの新しいインスタンス用のメモリが割り当てられます。そのメモリはまだ初期化されていません。
そのクラスの指定イニシャライザは、そのクラスが導入するすべての保存プロパティに値があることを確認します。これらの保存プロパティ用のメモリが初期化されます。
指定イニシャライザは、スーパークラスのイニシャライザに引き継ぎ、自身の保存プロパティに対して同じタスクを実行させます。
この作業は、クラス継承の連鎖の最上位に到達するまで続きます。
チェーンの最上位に到達し、チェーンの最終クラスが保存されているプロパティのすべてに値があることを確認したら、インスタンスのメモリは完全に初期化されたとみなされ、フェーズ1が完了します。

フェーズ2

チェーンの最上位から順に、チェーン内の各指定イニシャライザは、インスタンスをさらにカスタマイズすることができます。イニシャライザはselfにアクセスできるようになり、selfのプロパティを変更したり、selfのインスタンスメソッドを呼び出したりすることができます。
最後に、チェーン内の便利なイニシャライザは、インスタンスをカスタマイズし、selfを操作するオプションを持ちます。

以下は、仮想的なサブクラスとスーパークラスの初期化呼び出しのフェーズ1の様子です。

この例では、初期化はサブクラスのコンビニエンス・イニシャライザの呼び出しから始まります。この便利なイニシャライザは、まだどのプロパティも変更できません。このイニシャライザは、同じクラスの指定イニシャライザに処理を委ねます。

指定イニシャライザは、安全チェック1に従って、サブクラスのすべてのプロパティに値があることを確認します。その後、そのスーパークラスの指定イニシャライザを呼び出して、連鎖的に初期化を続けます。

スーパークラスの指定イニシャライザは、スーパークラスのすべてのプロパティに値があることを確認します。初期化すべきスーパークラスはそれ以上ありませんので、それ以上の委譲は必要ありません。

スーパークラスのすべてのプロパティが初期値を持つようになると、そのメモリは完全に初期化されたとみなされ、フェーズ1が完了します。

以下は、同じ初期化呼び出しに対するフェーズ2の様子です。

スーパークラスの指定イニシャライザには、インスタンスをさらにカスタマイズする機会があります(ただし、そうする必要はありません)。

スーパークラスの指定イニシャライザが終了すると、サブクラスの指定イニシャライザはさらにカスタマイズを行うことができます(ただし、この場合もそうする必要はありません)。

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

Initializer Inheritance and Overriding

Objective-C のサブクラスとは異なり、Swift のサブクラスはデフォルトではスーパークラスの初期化を継承しません。Swift のアプローチは、スーパークラスからの単純な初期化子が、より専門的なサブクラスに継承され、完全または正しく初期化されていないサブクラスの新しいインスタンスを作成するために使用される状況を防ぎます。

スーパークラスのイニシャライザーは特定の状況下で継承されますが、それは安全かつ適切な場合に限られます。詳細については、以下の「イニシャライザの自動継承」を参照してください。

カスタム サブクラスでスーパークラスと同じ初期化を使用する場合は、サブクラス内で初期化子のカスタム実装を行うことができます。

スーパークラスの指定イニシャライザと一致するサブクラスのイニシャライザを記述すると、事実上その指定イニシャライザのオーバーライドを提供することになります。そのため、サブクラスのイニシャライザ定義の前に override 修飾子を記述する必要があります。これは、「デフォルトイニシャライザ」で説明したように、自動的に提供されるデフォルトのイニシャライザをオーバーライドする場合にも当てはまります。

オーバーライドされたプロパティ、メソッド、またはサブスクリプトと同様に、override修飾子の存在は、オーバーライドされるマッチした指定イニシャライザをスーパークラスが持っていることをチェックするようSwiftに促し、オーバーライドされたイニシャライザのパラメータが意図したとおりに指定されていることを検証します。

スーパークラスの指定イニシャライザをオーバーライドする際には、サブクラスのイニシャライザの実装がコンビニエンス・イニシャライザであっても、常に override 修飾子を記述します。

逆に、スーパークラスのコンビニエンス イニシャライザに一致するサブクラスのイニシャライザを記述した場合、そのスーパークラスのコンビニエンス イニシャライザは、前述の「クラス型のイニシャライザの委譲」で説明したルールに従って、サブクラスから直接呼び出すことはできません。したがって、サブクラスはスーパークラスのイニシャライザのオーバーライドを提供しているわけではありません。そのため、スーパークラスの便利なイニシャライザの一致する実装を提供する際には、override修飾子を記述しません。

以下の例では、Vehicle というベースクラスを定義しています。この基底クラスは、numberOfWheelsという保存プロパティを宣言しており、デフォルトのInt値は0です。numberOfWheelsプロパティは、descriptionという計算プロパティによって使用され、車両の特性を表す文字列の説明が作成されます。

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

次の例では、VehicleのサブクラスであるBicycleを定義します。

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

Bicycleサブクラスは、カスタム指定イニシャライザであるinit()を定義しています。この指定イニシャライザは、Bicycleのスーパークラスの指定イニシャライザと一致するので、このイニシャライザのBicycleバージョンにはoverride修飾子が付いています。

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

Bicycle のインスタンスを作成すると、その継承された description computed property を呼び出して、numberOfWheels プロパティがどのように更新されたかを確認することができます。

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

サブクラスのイニシャライザが初期化プロセスのフェーズ2でカスタマイズを行わず、スーパークラスにゼロ引数の指定イニシャライザがある場合は、サブクラスの保存プロパティすべてに値を割り当てた後、super.init()の呼び出しを省略することができます。

この例では、Vehicleの別のサブクラスであるHoverboardを定義しています。そのイニシャライザでは、Hoverboardクラスはcolorプロパティのみを設定しています。このイニシャライザは、super.init()を明示的に呼び出すのではなく、スーパークラスのイニシャライザを暗黙的に呼び出して処理を完了させています。

    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)"
       }
   }

Hoverboardのインスタンスは、Vehicleイニシャライザによって提供されたデフォルトのホイール数を使用します。

   let hoverboard = Hoverboard(color: "silver")
   print("Hoverboard: \(hoverboard.description)")
   // Hoverboard: 0 wheel(s) in a beautiful silver
サブクラスは、初期化時に継承した変数プロパティを変更できますが、継承した定数プロパティは変更できません。

Automatic Initializer Inheritance

前述の通り、サブクラスはデフォルトではスーパークラスの初期化子を継承しません。しかし、スーパークラスのイニシャライザは、ある条件を満たすと自動的に継承されます。実際には、多くの一般的なシナリオでイニシャライザのオーバーライドを記述する必要はなく、安全であればいつでも最小限の労力でスーパークラスのイニシャライザを継承できることを意味しています。

サブクラスに導入する新しいプロパティにデフォルト値を提供すると仮定すると、次の2つのルールが適用されます。

ルール1
サブクラスが指定の初期化子を定義していない場合、スーパークラスの指定の初期化子をすべて自動的に継承します。

ルール2
サブクラスがスーパークラスのすべての指定イニシャライザの実装を提供する場合(ルール 1 に従って継承するか、定義の一部としてカスタム実装を提供する)、自動的にスーパークラスのすべての簡易イニシャライザを継承します。

これらの規則は、サブクラスがさらにコンビニエンス・イニシャライザを追加した場合でも適用されます。

サブクラスは、ルール 2 を満たす一環として、スーパークラス指定イニシャライザをサブクラス簡易イニシャライザとして実装できます。

Designated and Convenience Initializers in Action

次の例では、指定 イニシャライザ、コンビニエンス・イニシャライザ、および自動イニシャライザ継承の動作を示しています。この例では、Food、RecipeIngredient、ShoppingListItemという3つのクラスの階層を定義し、それらの初期化子がどのように相互作用するかを示しています。

階層の基本クラスはFoodと呼ばれ、食材の名前をカプセル化したシンプルなクラスとなっています。Foodクラスは、nameという1つのStringプロパティを導入し、Foodインスタンスを作成するための2つのイニシャライザを提供しています。

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

クラスにはメンバーごとのdefault memberwise initializerがないため、Foodクラスではnameという1つの引数を取る指定のイニシャライザを用意しています。このイニシャライザを使用して、特定の名前を持つ新しいFoodインスタンスを作成することができます。

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

Foodクラスのinit(name: String)イニシャライザは、新しいFoodインスタンスのすべての保存されたプロパティが完全に初期化されていることを保証するため、designated initializer,として提供されています。Foodクラスはスーパークラスを持っていないので、init(name: String)イニシャライザは、初期化を完了するためにsuper.init()を呼び出す必要はありません。

Foodクラスはまた、引数なしのコンビニエンス・イニシャライザ、init()を提供しています。init()イニシャライザは、Foodクラスのinit(name: String)に名前の値として[Unnamed]を渡すことで、新しい食べ物のデフォルトのプレースホルダー名を提供します。

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

階層構造の2番目のクラスは、RecipeIngredientというFoodのサブクラスです。RecipeIngredientクラスは、料理レシピの材料をモデル化しています。Foodから継承したnameプロパティに加えて、quantityと呼ばれるIntプロパティを導入し、RecipeIngredientインスタンスを作成するための2つのイニシャライザを定義しています。

    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)
       }
   }

RecipeIngredientクラスは、init(name: String, quantity: Int)という一つの指定されたイニシャライザを持ち、新しいRecipeIngredientインスタンスの全てのプロパティを入力するのに使用できます。このイニシャライザは、渡された quantity 引数を、RecipeIngredient によって導入された唯一の新しいプロパティである quantity プロパティに割り当てることから始まります。これを行った後、イニシャライザはFoodクラスのinit(name: String)イニシャライザに委ねます。このプロセスは、上記の二段階初期化の安全性チェック1を満たしています。

RecipeIngredientは、名前だけでRecipeIngredientインスタンスを作成するのに使用される便利なイニシャライザ、init(name: String)も定義しています。この便利なイニシャライザは、明示的な量なしに作成されたどんなRecipeIngredientインスタンスに対しても1の量を仮定します。このコンビニエンス・イニシャライザの定義は、RecipeIngredientインスタンスの作成をより早く、より便利にし、いくつかの単一量のRecipeIngredientインスタンスを作成する際のコードの重複を避けます。このコンビニエンス・イニシャライザは、単にクラスの指定イニシャライザに全体を委譲し、1の量の値を渡します。

RecipeIngredientによって提供されるinit(name: String)コンビニエンス・イニシャライザは、Foodのinit(name: String)指定イニシャライザと同じパラメータを取ります。このコンビニエンス・イニシャライザは、そのスーパークラスからの指定イニシャライザをオーバーライドするので、(イニシャライザの継承とオーバーライドで説明されているように)override修飾子でマークされなければなりません。

たとえRecipeIngredientがinit(name: String)イニシャライザをコンビニエンス・イニシャライザとして提供していても、RecipeIngredientはそれにもかかわらず、そのスーパークラスの指定されたイニシャライザのすべての実装を提供しています。したがって、RecipeIngredientは自動的にそのスーパークラスのすべてのコンビニエンス・イニシャライザも継承します。

この例では、RecipeIngredientのスーパークラスはFoodで、それはinit()と呼ばれる1つのコンビニエンス・イニシャライザを持っています。このイニシャライザは、RecipeIngredientによって継承されます。継承されたバージョンのinit()は、Foodバージョンではなく、RecipeIngredientバージョンのinit(name: String)にデリゲートすることを除いて、Foodバージョンと全く同じように機能します。

これら3つのイニシャライザは、新しいRecipeIngredientインスタンスを作成するために使用することができます。

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

階層の3番目と最後のクラスは、RecipeIngredientのサブクラスであるShoppingListItemです。ShoppingListItemクラスは、ショッピングリストに表示されるレシピ成分をモデル化しています。

ショッピング・リストのすべてのアイテムは、「未購入」として始まります。この事実を表現するために、ShoppingListItemには、デフォルト値がFalseのBooleanプロパティであるpedasedが導入されています。また、ShoppingListItemには、計算プロパティDescriptionが追加され、ShoppingListItemインスタンスのテキスト記述を提供します。

    class ShoppingListItem: RecipeIngredient {
       var purchased = false
       var description: String {
           var output = "\(quantity) x \(name)"
           output += purchased ? " ✔" : " ✘"
           return output
       }
   }
ShoppingListItemは、ショッピング・リスト(ここでモデル化されている)のアイテムは常に購入されていない状態で開始されるため、購入済みの初期値を提供するイニシャライザを定義していません。

ShoppingListItemは、導入するすべてのプロパティにデフォルト値を提供し、それ自体にイニシャライザを定義していないため、スーパークラスから指定およびコンビニエンス・イニシャライザをすべて自動的に継承します。

継承された3つのイニシャライザをすべて使用して、新しいShoppingListItemインスタンスを作成することができます。

    var breakfastList = [
       ShoppingListItem(),
       ShoppingListItem(name: "Bacon"),
       ShoppingListItem(name: "Eggs", quantity: 6),
   ]
   breakfastList[0].name = "Orange juice"
   breakfastList[0].purchased = true
   for item in breakfastList {
       print(item.description)
   }
   // 1 x Orange juice ✔
   // 1 x Bacon ✘
   // 6 x Eggs ✘

ここでは、3 つの新しい ShoppingListItem インスタンスを含む配列リテラルから breakfastList という新しい配列が作成されています。配列のタイプは [ShoppingListItem] と推測されます。配列が作成されると、配列の先頭にある ShoppingListItem の名前が "[Unnamed]" から "Orange juice" に変更され、購入されたものとしてマークされます。配列内の各アイテムの説明をprint()でコンソール出力すると、デフォルトの状態が期待通りに設定されていることがわかります。

この記事が気に入ったらサポートをしてみませんか?