Swiftで遊ぼう! - 407 - Initializer イニシャライザ
- Swiftで遊ぼう!の前書き-> Life-LOG OtherSide
- 初心者はここから!-> 50オヤジでもできるiOS開発
- 私の本業、オフィシャルなブログ-> Life-LOG
- Swift 3 対応
2016年11月5日:やっと失敗許容イニシャライザが理解できました。
Swift 3向けにアップデイトされたThe Swift Programming Language本に書かれているクラスのイニシャライザに関する項目を読んでいます。あしたさぬき版のSwiftで遊ぼう!でイニシャライザの説明が中途半端だったので、昔の記事を改訂しながらページをまとめてみます。
Swiftで遊ぼう! - 22 クラスの初期化は複雑なステップ
Swiftで遊ぼう! - 25 イニシャライズの具体例をやっと始めよう
Swiftで遊ぼう! - 169 - Initializer 1
Swiftで遊ぼう! - 170 - Initializer 2
Swiftで遊ぼう! - 171 - Initializer 3 クラスイニシャライザー
イニシャライザが理解できず、かなり悩んだ日々が続いていました。理解するのに時間がかかりましたが、今は完璧と言えないまでも、ほぼ完全に理解できています。
The Swift Programming Language本のInitializersのパートを復習する感覚で読むことができました。
まず表記の統一をした方がいいでしょう。日本語表記と英語表記の混在になると思いますが、それぞれは下記のようにします。
- 指定イニシャライザ(designated initializer)
- 簡易イニシャライザ(convenience initializer)
- 失敗許容イニシャライザ(failable initializer)
- 必須イニシャライザ(required initializer)
「指定イニシャライザ」と「簡易イニシャライザ」
指定イニシャライザは必ずクラスに1つ必要です。何が重要かと言えば、これ1つで、そのクラスが独自(スーパークラスから継承されてくるプロパティは含まれません)に持っているプロパティ全ての値を持たせることができるという点です。そして引数のとり方を変更して複数の指定イニシャライザも設定できます。スーパークラスを継承しているのであれば、必ずスーパークラスの指定イニシャライザーを呼びます。下記のように記述します。
init(parameters) {
statements
}
簡易イニシャライザは、オプション的に付け加えることができます。このイニシャライザの特徴は、同じクラスの他のイニシャライザを必ず呼ばなければならず、最終的に指定イニシャライザを呼ん(self)で同クラス内のプロパティ全ての初期値を指定します。下記のように記述します。
convenience init(parameters) { statements }
イニシャライザの関係性
どのイニシャライザを最後にインスタンス化させるか、指定イニシャライザと簡易イニシャライザの関係性を理解するために、次の3つのルールがあります。
- 指定イニシャライザは、直属のスーパクラスにある指定イニシャライザを呼ばなければならない。
- 簡易イニシャライザは、同クラスの他のイニシャライザ(指定、簡易どちれでも)を呼ばなければならない。
- 簡易イニシャライザは、最終的に指定イニシャライザを呼ばなければならない。
必ず最上位のスーパークラスにある指定イニシャライザで終わるようになっています。
初期化のセイフティーチェック
Safety check 1
指定イニシャライザは、継承元のスーパークラスのプロパティの初期化に上がる前に、そのクラスが持っているプロパティ全てを必ず初期化しなければならない。
Safety check 2
指定イニシャライザは、継承したプロパティに値を入れる前に、スーパークラスのイニシャライザを実行させなければならない。
Safety check 3
簡易イニシャライザは、どんなプロパティに値を入れる前に他のイニシャライザを実行させなければならない。
Safety check 4
イニシャライザは、インスタンスメソッドやインスタンスプロパティの値を読み出したりできない。
そしてこのセイフティーチェックを考えながら2つのフェイズを経てクラスのインスタンス化が成功する。
フェイズ1
初期化の継承とoverriding
スーパークラスのプロパティを変更したいときに、スーパークラスの持っている指定イニシャライザをそのまま変更しないで呼んでから、プロパティの書き換え(override)をします。キーワードとして「override」をつけます。
class Vehicle { var numberOfWheels = 0 var description: String { return "\(numberOfWheels) wheel(s)" } } class Bicycle: Vehicle { override init() { super.init() numberOfWheels = 2 } } 抜粋:: Apple Inc. “The Swift Programming Language (Swift 3.0.1)”。 iBooks https://itun.es/jp/jEUH0.l
Automatic Initializer Inheritance
サブクラスは、スーパークラスのイニシャライザを継承しません。しかしながら特定の条件で、スーパークラスのイニシャライザは自動的に継承されることがあります。ということは、自分で「override」を書いてイニシャライザをコードする必要性がないということです。この特定の条件には以下の2つのルールがあります。
ルール1
サブクラスで指定イニシャライザを定義しなかった場合、自動的にスーパークラスの指定イニシャライザが継承されます。
ルール2
サブクラスがスーパークラスの持っている指定イニシャライザ全てを実行できる場合、ルール1のイニシャライザ継承によろうが、その定義の一部として独自に実行させるとしても、自動的にスーパークラスの簡易イニシャライザは継承されます。
ちょっと分かりにくいので実例とともに解説します。
class Food { var name: String init(name: String) { self.name = name } convenience init() { self.init(name: "[Unnamed]") } } 抜粋:: Apple Inc. “The Swift Programming Language (Swift 3.0.1)”。 iBooks https://itun.es/jp/jEUH0.l
Foodクラスは継承するスーパークラスを持っていないので、super.init()は要りません。すると指定イニシャライザが必要になります。init(name: String)が指定イニシャライザーになります。このクラスを引数無しで初期化するinit()を簡易イニシャライザとして与えているので、自分の持っているself.init(name:)を呼ばなければなりません。
次にこのFoodクラスを継承したRecipeIngredientクラスを定義します。
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) } } 抜粋:: Apple Inc. “The Swift Programming Language (Swift 3.0.1)”。 iBooks https://itun.es/jp/jEUH0.l
RecipeIngredientクラスも指定イニシャライザを1つだけinit(name: String, quantity: Int)を持っています。このイニシャライザだけで、全てのプロパティを指定することができます。このイニシャライザはまず、独自のプロパティquantityの割り付けから始まります。それが終わってからスーパークラスのinit(name: String)を使ったFoodクラスの初期化ステップが生じます。
init(name: String)はRecipeIngredientクラスの簡易イニシャライザですが、Foodクラスの指定イニシャライザなので、スーパークラスの指定イニシャライザを書き換え(overriding)が生じているので「override」が必ず必要になります。そしてRecipeIngredientクラスの簡易イニシャライザではありまずが、スーパークラスの指定イニシャライザを提供することになったので、自動的にスーパークラスの簡易イニシャライザが継承されます。
let oneMysteryItem = RecipeIngredient() // oneMysteryItem.name = [unnamed] // oneMyeteryItem.quantity = 1
次はRecipeIngredientクラスを継承したShoppingListItemクラスを次のように用意します。
class ShoppingListItem: RecipeIngredient { var purchased = false var description: String { var output = "\(quantity) x \(name)" output += purchased ? " ✔" : " ✘" return output } }
2つのプロパティ、Boolean型のpurchasedとsString型の計算型プロパティのdescriptionは、デフォルトで値がセットされるため独自のイニシャライザを持っていません。するとAutomatic Initializer Inheritanceのルール1が適用されて、スパークラスの指定イニシャライザが継承されます。更に指定イニシャライザが継承されると、ルール2も適用されるのでスーパークラスの簡易イニシャライザも継承されます。
var breakfastList = [ ShoppingListItem(), ShoppingListItem(name: "Bacon"), ShoppingListItem(name: "Eggs", quantity: 6), ] breakfastList[0].name = "Orange" breakfastList[0].purchased = true for item in breakfastList { print(item.description) }
イニシャライザの理解が進みました。RecipeIngredient()が用意しているすべての指定、簡易イニシャライザーが使えます。
失敗許容イニシャライザ(failable initializer)
イニシャライザでエラーが生じると実行時にエラーが生じてアプリケーションは破綻します。引数を受け取るイニシャライザの場合、存在しない(nil)引数が渡されるとエラーが生じるので、nilでも受け取れる失敗許容イニシャライザが用意されています。initの後に「?」をつけます。正本では失敗許容イニシャライザで使ったパラメータの名前と型が同じ普通のイニシャライザを定義することはできないと書いてあります。そりゃ当然ですよね。というより最上流のスーパークラスのコーディングは全て失敗許容イニシャライザにするとエラーを回避しやすいと思うけどどうなんでしょうね。
次の例をみていきます。
class Product { let name: String init?(name: String) { if name.isEmpty { return nil } self.name = name } } class CartItem: Product { let quantity: Int init?(name: String, quantity: Int) { if quantity < 1 { return nil } self.quantity = quantity super.init(name: name) } }
有効な引数が得られなかったところで初期化ステップはたちどころに中断されます。CartItemクラスの初期化を見れば、quantityに有効な値、0なんかが入力されると、そこで初期化ステップは終わります。nameが有効じゃない場合、super.initが呼ばれたところで中断されます。
そして失敗許容イニシャライザをオーバーライドすることもできます。スーパークラスの失敗許容イニシャライザをサブクラスでオーバーライドする場合、失敗できないイニシャライザ(要するに普通のイニシャライザ)に変更できますが、その場合失敗は許されないし、スーパークラスのオプションを強制的にアンラップさせなければなりません。
class Document { var name: String? init() {} init?(name: String) { if name.isEmpty { return nil } self.name = name } } class AutomaticallyNamedDocument: Document { override init() { super.init() self.name = "[Untitled]" } override init(name: String) { super.init() if name.isEmpty { self.name = "[Untitled]" } else { self.name = name } } } class UntitledDocument: Document { override init() { super.init(name: "[Untitled]")! } }
まさに、プロパティのオプショナル型と同じ扱いだと考えると失敗許容イニシャライザも理解できますね。「init?」と「
必須イニシャライザ(required initializer)
「required」のついているイニシャライザには以下の特徴があります。
- 継承時にかならず実装しなければなりません。
- 書き換え(override)ができますが、このとき「override」のキーワードは要りません。
- 継承しても必ず「required」が必要です。
class SomeClass { required init() { // ここで実装 } } class SomeSubclass: SomeClass { required init() { // ここで書き換えができます。 } }
この必須イニシャライザで注意しなければならないことが1つあります。プロトコールにイニシャライザを記述する場合、プロトコールに準拠するクラスは必ずこのイニシャライザを実装しなければならないのである意味必須イニシャライザです。プロトコールに記述するイニシャライザに「required」は必要ないのですが、準拠するクラスで実装時に「required」を書かなければなりません。