読者です 読者をやめる 読者になる 読者になる

Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 407 - Initializer イニシャライザ

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つのルールがあります。

  1. 指定イニシャライザは、直属のスーパクラスにある指定イニシャライザを呼ばなければならない。
  2. 簡易イニシャライザは、同クラスの他のイニシャライザ(指定、簡易どちれでも)を呼ばなければならない。
  3. 簡易イニシャライザは、最終的に指定イニシャライザを呼ばなければならない。

f:id:yataiblue:20150812160307j:plain

必ず最上位のスーパークラスにある指定イニシャライザで終わるようになっています。

初期化のセイフティーチェック

Safety check 1
指定イニシャライザは、継承元のスーパークラスのプロパティの初期化に上がる前に、そのクラスが持っているプロパティ全てを必ず初期化しなければならない。
Safety check 2
指定イニシャライザは、継承したプロパティに値を入れる前に、スーパークラスのイニシャライザを実行させなければならない。
Safety check 3
簡易イニシャライザは、どんなプロパティに値を入れる前に他のイニシャライザを実行させなければならない。
Safety check 4
イニシャライザは、インスタンスメソッドやインスタンスプロパティの値を読み出したりできない。

そしてこのセイフティーチェックを考えながら2つのフェイズを経てクラスのインスタンス化が成功する。

フェイズ1

  1. クラスの指定もしくは簡易イニシャライザが呼ばる。
  2. 新しいインスタンスに対してメモリーの初期化が始まる
  3. 同じクラスのプロパティに値が入れられメモリーも割りふられる
  4. スーパークラスのイニシャライザに移ってプロパティに値が組み込まれメモリーも割りふられる
  5. この作業が最上位のクラスまで遡って初期化がすすむ
  6. 一番最上位のクラスのイニシャライザが実行されてすべてのプロパティに値が保持されたときにメモリーの初期化が終了してフェイズ1が終了となる

フェイズ2

  1. プロパティのカスタマイズに入る。上位の指定イニシャライザから下っていきながらプロパティの値を変更していく。ここにきてself.プロパティやインスタンスメソッドを呼ぶ
  2. 最後に簡易インスタンスのカスタマイズで終了

初期化の継承と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」のついているイニシャライザには以下の特徴があります。

  1. 継承時にかならず実装しなければなりません。
  2. 書き換え(override)ができますが、このとき「override」のキーワードは要りません。
  3. 継承しても必ず「required」が必要です。
class SomeClass {
    required init() {
        // ここで実装
    }
}

class SomeSubclass: SomeClass {
    required init() {
        // ここで書き換えができます。
    }
}

この必須イニシャライザで注意しなければならないことが1つあります。プロトコールにイニシャライザを記述する場合、プロトコールに準拠するクラスは必ずこのイニシャライザを実装しなければならないのである意味必須イニシャライザです。プロトコールに記述するイニシャライザに「required」は必要ないのですが、準拠するクラスで実装時に「required」を書かなければなりません