iBooks にある “The Swift Programming Language” の勉強メモ。Objective-C と C を普段書いている自分から、ちょっと馴染みがないものを特にまとめておきます。目次は こちら。
今回は、前回 の続きのクラスの Initialization に関して。
Initialization
継承することができるクラスと、継承することができない構造体・Enumeration では、初期化を定義する際に違いでてきます。前回までに、初期化の一般的なことと、構造体・Enumeration の初期化をみたので、この記事では、クラスの初期化に関してみていきます。最後に、プロパティをクロージャや関数を使用して初期化する方法も軽く触れます。
5. クラスの継承と Initializer
次にクラスの継承と Initializer をみていきます。クラスのすべての Stored プロパティは(スーパークラスのプロパティも含めて)、初期化時に初期値が与えられなければなりません。クラスの Initializer は Designed Initializer と Convenience initializers に分けることができます。
Designated initializers はメインの Initializer となります。この Initializer は、すべてのプロパティの初期化をし、必要であればスーパークラスの Initializer も呼ぶ必要があります。全てのクラスは最低でも 1 つの Designated initializers を持つ必要があります。
Convenience initializers は いくつかのデフォルト値と一緒に Designated initializers を呼びます。
これらの関係を単純化するため、以下のルールを覚えておくと分かりやすいです。
- ・Designated initializers は、スーパークラスの Designated initializers を呼ぶ
- ・Convenience initializers は、同じクラス内にある他の Initializer を呼ぶ
- ・Convenience initializers は、最終的に Designated initializers を呼ぶ
つまり、
- ・Designated initializers は delegate up (スーパークラスのinit)
- ・Convenience initializers は delagate across (同じクラスのinit)
と覚えておくとよいです。
このルールの関係性を維持し、それぞれの Initializer が仕事をすれば複雑なクラス間の初期化も対応することができます。下の画像を見てもらうと分かりますが、どの Initializer が呼ばれても漏れなく初回化できる流れが掴めると思います。
6. クラスの二段階初期化
Swift におけるクラスの初期化は大きく二段階に分けて行われます。最初のフェーズでは、全ての Stored プロパティ に初期値が設定されます。そして、次のフェーズでは、クラスはこれらの Stored プロパティを独自に設定しなおす機会が与えられます。
フェーズ 1
-
- designated or convenience initializer が呼ばれる。
-
- 新しいインスタンスへのメモリが割り当てられる。まだ、初期化はされていない。
-
- designated initializer が全ての Stored プロパティ に値が設定されるか確認。この時に、Stored プロパティのためのメモリが初期化される。
-
- この designated initializer は、スーパークラスの initializer へ、その Stored プロパティに対して、初期化するために処理を渡す。
-
- これが必要なくなるまで最後まで上に行く。
-
- 最後までこの流れが続き、最後のクラスの全ての Stored プロパティが値を持ったら、このインスタンスは完全に初期化されフェーズ 1 の終了。
フェーズ 2
-
- フェーズ 1 での最後のクラスから処理が戻ってきて、各 designated initializer は、独自の処理を実行できる。Initializer は、
self
を使うことができ、そのプロパティを修正でき、そのクラスのインスタンスメソッドを呼ぶことができる。
- フェーズ 1 での最後のクラスから処理が戻ってきて、各 designated initializer は、独自の処理を実行できる。Initializer は、
-
- 最後に、convenience initializer が独自の処理を実行できる。(
self
を使用可能)
- 最後に、convenience initializer が独自の処理を実行できる。(
これが、二段階初期化の流れです。
7. 初期化処理の継承
Swift は、サブクラスはデフォルトでスーパークラスの Initializer を継承しません。これは、単純なスーパークラスの Initializer が 複雑な処理を必要とするサブクラスから継承されて、そのサブクラスが新しいインスタンスを作成する際、まだ初期化されていな値を使用してしまうといったエラーを防ぐためです。
サブクラスにおいて、スーパークラスを同じ Initializer を使用したい場合には、オーバーライドします。もし、オーバーライドする Initializer が、designed initializer であれば、この実装をオーバーライドして、スーパークラスの Initializer をこのオーバーライドされたものから呼ぶことができます。convenience initializer をオーバーライドする際には、必ずそのサブクラスから designated initializer を呼ぶ必要があります。オーバーライドする時に、override
キーワードを使用する必要はありません。
このように、デフォルトでは Initializer は継承されませんが、ある条件を満たすと継承されます。実質は、多く場合自動的に継承されることが多いようです。
Rule 1
もしサブクラスが designated initializer を持たない場合、このサブクラスは自動的に全てのスーパークラスの designated initializer を継承します。
Rule 2
もしサブクラスがスーパークラスの全ての designated initizliser を実装した場合 (Rule 1 の場合の継承も含む)、スーパークラスの全ての convenience initializer は継承されます。
8. Designated and Convenience Initializers のシンタックス
Designated initializer Convenience Initializer は、init の前に、convenience
キーワードを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 クラスは、1 つの Designated initializer と、1 つの Convenience Initializer を持ちます。わかりやすいと思います。
1 2 3 4 5 6 7 8 9 10 | class RecipeIngredient: Food { var quantity: Int init(name: String, quantity: Int) { self.quantity = quantity super.init(name: name) } convenience init(name: String) { self.init(name: name, quantity: 1) } } |
この RecipeIngredient は Food クラスを継承し、1 つの Designated initializer と、1 つの Convenience Initializer を持っています。さらに、RecipeIngredient は Food の Convenience Initializer である、init() を継承しています。これは、7 でみた Rule 2 によるものです。RecipeIngredient は、Convenience Initializer として、init(name: String) を実装しています。これは、Food クラスの Designated initializer と同じシグニチャです。これにより、Food クラスの Designated initializer の全ての実装していることになるので、Food の Convenience Initializer を継承します。
1 2 3 | let oneMysteryItem = RecipeIngredient() let oneBacon = RecipeIngredient(name: "Bacon") let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6) |
このように 3 つの Initializer を使用できます。
1 2 3 4 5 6 7 8 | class ShoppingListItem: RecipeIngredient { var purchased = false var description: String { var output = "\(quantity) x \(name.lowercaseString)" output += purchased ? " ✔" : " ✘" return output } } |
最後に、ShoppingListItem クラス。このクラスは、1 つの新しいプロパティを持ちます。これは初期化され際には、常に false です。そのため、このクラスに対して、特に Initializer は必要がありません。その結果、Rule 1 により、全ての Initializer を継承します。
9. クロージャー・関数でのデフォルトプロパティ値の設定
Stored プロパティがデフォルト値を持ち、その値を決める際にちょっとした処理が必要な場合には、クロージャーかグローバルな関数を使用できます。このインスタンスが初期化される際に、これらの処理が呼ばれます。
1 2 3 4 5 6 7 | class SomeClass { let someProperty: SomeType = { // create a default value for someProperty inside this closure // someValue must be of the same type as SomeType return someValue }() } |
これがその基本形です。注意点として、最後に ()
があります。これのより、このクロージャーをすぐに呼び出すということを明示しています。これがないと、このプロパティにクロージャーを設定するという意味になってしまいます。また、このクロージャーの中では、他のインスンスプロパティにまだアクセスできません。まだ初期化が終わっていないからです。self
も使えません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct Checkerboard { let boardColors: Bool[] = { var temporaryBoard = Bool[]() var isBlack = false for i in 1...10 { for j in 1...10 { temporaryBoard.append(isBlack) isBlack = !isBlack } isBlack = !isBlack } return temporaryBoard }() func squareIsBlackAtRow(row: Int, column: Int) -> Bool { return boardColors[(row * 10) + column] } } let board = Checkerboard() println(board.squareIsBlackAtRow(0, column: 1)) // prints "true" println(board.squareIsBlackAtRow(9, column: 9)) // prints "false" |
この例では、boardColors
を設定するためにクロージャーを使用しています。チェスのボードの白黒のマス目を設定するためです。