https://www.youtube.com/watch?v=O21UPvuL9C4
引き続き A Swift Tour
の「オブジェクトとクラス」を見ていきますけれど、今回はその中の 継承におけるイニシャライザーの実装例
がテーマになります。
これについては最近の中でも話す機会はありましたけれど、何かのついでに余談的に話す感じでしたので、せっかくなので改めてこの継承におけるイニシャライザーあたりについて真っ直ぐ着目して眺めていってみようと思います。どうぞよろしくお願いしますね。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #50
00:00 開始 02:21 継承におけるイニシャライザー 06:10 初期化・代入・参照 09:59 初期化フェーズ 11:02 継承における初期化フェーズ 14:04 初期化のカスタマイズ 17:04 プロパティー宣言時の初期化 18:29 self と super の関係 25:50 指定イニシャライザー 27:22 便宜イニシャライザー 33:55 イニシャライザーの継承と隠蔽 41:39 必須イニシャライザー 44:34 定数に2回代入してみる ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #50
はい、今日はイニシャライザについてお話します。特に継承におけるイニシャライザについて見ていく回にしようと思います。
スライドはApple公式の「The Swift Programming Language」という文章を基にしています。今回はオブジェクトとクラスに関する話の中で、継承におけるイニシャライザの実装例を紹介します。ここは重要なポイントで、オブジェクト指向においてインスタンスの入り口となるイニシャライザは非常に複雑な要素を持っています。
これまでの話に加え、この回でそのあたりをまとめるつもりですので、すでに理解している方も、新しいことを学ぶために、ぜひ聞いてみてください。まずはスライド、本に記載されている内容をざっくりと見ていきましょう。
具体的な例として、EquilateralTriangle
(正三角形)がNameShape
というクラスから継承されているコードを見てみます。NameShape
は名前付きの図形クラスです。このクラスから正三角形のクラスを作るときのイニシャライザを見ていきます。
今回のEquilateralTriangle
クラスが派生クラスで、NameShape
が基本クラスです。このクラスのイニシャライザでは、まず派生クラスで宣言されているプロパティの設定が行われます。その後、親クラスのイニシャライザを呼ぶことで、親クラスのプロパティも初期化されます。
具体例として、NumberOfSides
はNameShape
の中で定義されたプロパティですが、親のプロパティを初期化した後であれば、派生クラスのイニシャライザ内でその値を変更することも可能です。このような動きが、派生クラスのイニシャライザになります。
これを理解するのは少し難しいかもしれませんが、細かく見ていきましょう。インスタンスを生成する際には、イニシャライザフェイズ、つまり変数に値を代入するフェイズがあります。例えばプレイグラウンドで見ていくと、クラスがあって、イニシャライザがあり、let obj = Object()
のように変数を宣言して使っていく場面があります。このとき、以下の三つのフェイズがあります:
- 宣言(Declaration): まず変数が宣言されます。例えば、
var value
のように。 - 初期化(Initialization): 変数に具体的な値が入れられます。例えば、
value = 10
のように。 - 割り当て(Assignment): 最初の初期化の後に、値を変更することです。
例えば、var value: Int
で変数を宣言し、初めて値を代入するのが初期化で、value = 10
という形になります。その後で値を代入するのが割り当て(アサイン)となります。リファレンスは値を参照するときです。
以上が基本的なイニシャライザの流れとなります。これを踏まえて、継承におけるイニシャライザの具体的な実装について改めて見ていきましょう。 この「リファレンス」という言葉は、ここで言っている変数を参照するのとはまた別の意味で使われています。参照型(リファレンス)のリファレンスとはちょっと意味が違いますが、とにかくこの3つのフェーズ、つまり変数を扱う上での初期化フェーズと代入フェーズは重要なポイントとなっています。
Swiftでは10行目と11行目が少し分かりにくいかもしれませんが、初期化フェーズと代入フェーズが別物であるということをなんとなく意識しておくと良いと思います。大事なポイントとして、イニシャライザーが担当するのはこの初期化フェーズになります。
初期化フェーズでは、インスタンスを完全に使える状態にする役割を持っています。現代のプログラミング言語では、インスタンスを使える状態にするには、プロパティを適切な状態にしておく必要があります。クラスの検証時の話として、例えばベースクラスがAとBを持っていて、サブクラスがベースクラスを継承してCとDを持っているとします。この場合、自分自身のイニシャライザーでは自分のプロパティだけを初期化し、親のイニシャライザーに初期化を委ねるのが基本です。
オーバーライドのポイントとして、親が持っているプロパティAの初期化を自分のイニシャライザーでは行わず、親クラスのイニシャライザー内で初期化します。また、自分のプロパティは自分自身のイニシャライザーで初期化するべきです。この役割を持っているイニシャライザーをデザインイニシャライザーと呼びます。
継承時のポイントとして、親クラスの初期化フェーズは親クラスのイニシャライザー内で完了し、子クラスの初期化フェーズは子クラスのイニシャライザー内で完了します。その中で必ず親のイニシャライザーを呼ぶ必要があります。初期化フェーズは、親のイニシャライザーが呼ばれた後も続きます。このため、親クラスのイニシャライザーが実行された後でもプロパティに再代入することが可能です。
25行目に相当する部分がカスタマイズフェーズと呼ばれます。言語仕様的には、再代入することは悪いことではありません。ただし、処理が無駄になる可能性があり、パフォーマンスにも影響します。しかし、パフォーマンス重視のコードでない場合、この書き方も自然なコードとして言語仕様に適っています。
もう一つ大事なポイントとして、クラスの初期化フェーズでは、イニシャライザーの外側でもプロパティに初期値を代入することが含まれます。 どうも、この書き方はJava言語でいうところのイニシャライザーですね。こちらの18行目からのコードはJavaでいうコンストラクタのような印象を持っています。いずれにしても、この2つで初期化フェーズを構成します。
今回の例で言えば、まずインスタンス化されるときにアロケートが終わり、イニシャライザーが呼ばれる前に変数 C
に1が代入されます。その後、イニシャライザーが実行されて改めて C
に0が代入され、 D
が0になるという流れになります。ここで D
の値が2回書かれて、最初1になり次に0になるところは注意が必要です。しかし、基本的に初期化のフェーズはこのように2段階で進んでいきます。
「スーパードットユニット」を使っているということは、このサブクラスのインスタンスの中に親クラスのインスタンスも持っているのかどうかという質問がありました。親クラスとサブクラス、両方基本的には一緒です。厳密には、親クラスのプロパティやメソッドもサブクラスが引き継ぐ形になります。
自分も以前は同じように考えていましたが、改めてじっくり見ると「スーパードットユニット」で親クラスを初期化しているので、サブクラスから親クラスのメソッドを呼び出すことができます。したがって、親クラスのインスタンスも作られているように見えるが、実際には一つのインスタンスの中で親クラスのプロパティやメソッドを共有している形です。
Swiftのクラス管理は仮想テーブルを使います。例えば、let a = function()
や unsafeBitCast
などを使ってキャストする方法で、親クラスと子クラスのインスタンスが同じものであることも確かめることができます。この結果、サブクラスは親のインスタンスを持っているわけではなく、サブクラス自身が親クラスのインスタンスも含んでいると考えられます。
これがインスタンスのポリモーフィズム(多態性)やサブタイピングの特徴です。後ほどカスタマイズやメモリ管理などについても詳しく見ていきましょう。
以上のように、クラス継承の際にイニシャライザーを作成する場合、まず自分自身の初期化を先に行い、その後親クラスのプロパティを初期化するという流れになります。クラス継承において、親クラスのプロパティも自分自身が持つことになります。 なので、継承されているものも含めた中で、自分自身に定義されているものの初期化を行います。その後、親に定義されていたものの初期化は親にやらせて、最後に自分の中でカスタマイズや調整を行うという流れになります。これによってインスタンスが完全に初期化されます。特に、継承されたクラスのインスタンスが確実に初期化できたとなり、これでイニシャライズフェーズが終わる役割を担っているのがディジグネイティブイニシャライザーです。
これがとても大事なポイントになっていて、クラスの初期化は必ずディジグネイティブイニシャライザーが行います。ただし、便利的にいろんな加工を施したり、または独自のカスタマイズをしたいときにコンビニエンスイニシャライザーが使えます。例えば、C
とD
を受け取る通常のイニシャライザーがあって(例としてx
を受け取る形にして)、このx
で受け取った値を両方に設定するようなコードを書きたいときに、コンビニエンスイニシャライザーが威力を発揮します。
まず、コンビニエンスイニシャライザーの大事な役目としては自分自身が持っているイニシャライザーに初期化処理を委ねる役目があります。ですので、この中で必ず自分自身のイニシャライザーを呼ばないといけないというルールがあります。これがsuper
を呼ぼうとするとエラーになる理由です。コンビニエンスイニシャライザーは必ず自分自身のイニシャライザーを呼ばないといけません。
例えば、以下のようなコンビニエンスイニシャライザーがあります。
convenience init(x: Int) {
self.init(p: x, d: x)
}
このように、今用意したディジグネイティブイニシャライザーはプロパティをすべてゼロに初期化する機能しか持っていないので、x
の値を設定したい場合はカスタマイズフェーズのほうでp
にx
を入れるといったカスタマイズが必要です。
init(p: Int, d: Int) {
self.p = p
self.d = d
}
このように複数のディジグネイティブイニシャライザーを用意しても良いですが、使い回せるなら使い回したほうが効率的です。コンビニエンスイニシャライザーは以下のように書くこともできます。
convenience init() {
self.init(p: 0, d: 0)
}
こうすることで、バグの少ないコードが書けるようになります。クラスの場合、コンビニエンスイニシャライザーかディジグネイティブイニシャライザーかを明示することが初期化のルールとして重要です。ディジグネイティブイニシャライザーがクラスを完璧に初期化することを保証するためです。
このように、ディジグネイティブイニシャライザーとコンビニエンスイニシャライザーの違いを理解して使うことで、イニシャライザーの扱い方が変わり、プログラミングがより面白くなると思います。 さらにこれを継承してみましょうか。ベースサブクラスのさらにサブクラスを「サブサブ」にしましょうかね。
さて、サブ
クラスのさらなるサブクラスであるサブサブ
クラスを継承したときについて前回か前々回のお話を思い出してみます。リクアイアドイニシャライザーについてお話ししようと思います。
まず、サブサブクラス
を初期化しようとすると、引数を取らないもの(デフォルトイニシャライザー)、d
を取るもの、x
を取るものと、つまりサブクラス
で定義されているイニシャライザーが3つ使える状態になります。オブジェクト指向ではこのクラスの機能を継承して使えるという特徴があります。なので、何も実装していなくてもそのイニシャライザーが使えるというポイントです。
例えば、ここでパー
というプロパティを持ったとします。この場合、サブクラス
のイニシャライザーは何もなくなります。というのは、サブクラス
で定義されているプロパティを、サブサブクラス
で初期化してあげなければならないからです。
理由は簡単で、サブクラス
ではサブサブクラス
に継承されてプロパティが付加されるなんてことは想定できないからです。なので、このクラスでイニシャライザー中に継承先も踏まえた初期化を完全に行うことは不可能となります。
もしプロパティE
をサブサブクラス
に追加したときに、以前のサブクラス
で定義されているイニシャライザーが呼べてしまうと、プロパティE
が完全に初期化できず初期化漏れが発生してしまいます。そのため、これが隠蔽されるわけです。
このとき、サブクラス
がイニシャライザーを何かしら定義して、自分自身を責任を持って初期化し、そして親クラスに初期化を委ねるように設計されます。サブサブクラス
は自分自身で実装したイニシャライザーだけが呼べるというものになります。
ここで大事な点としては、プロパティ
を特に定義しなかった場合、何もイニシャライザーを定義しなかったときには、サブサブクラス
のイニシャライザーは親クラスのものが使えるということです。ただし、サブサブクラス
内で何かしらのイニシャライザーを定義して親のイニシャライザーを呼び出した場合、そのサブサブクラスのイニシャライザーのみが使えるようになります。
例えば、親のイニシャライザーが使える状態でも、サブサブクラス
がプロパティを持っていないため、特に初期化の観点からは問題はありません。ただし、値が設定されているかどうかという点は問題にはなりません。しかし、サブサブクラス
に自分自身を初期化する責任を持っているデリゲートイニシャライザーがあるということは、サブサブクラス
独自の初期化があるのだろうということになります。
そうすると、継承元のイニシャライザーではサブサブクラス
を想定した初期化ができない可能性が発生します。特定の初期化が必要な場合には、独自のイニシャライザーで組み立てられるクラスになります。
これがデリゲートイニシャライザーではなく、コンビニエンスイニシャライザーを実装した場合には、スーパーには行かず、自分自身のイニシャライザーに初期化を依頼します。こうして初期化を書いてあげると、この場合には親クラスのイニシャライザーと独自に実装したイニシャライザーが全て使える状態になります。
コンビニエンスイニシャライザーはちょっとした加工で、絶対的な責任を持つのはデリゲートイニシャライザーです。そのため、特別な初期化が基本的にはなく、親クラスの初期化を使うことでサブクラスも初期化できます。それを少し便利にした感じですね。このようにしてイニシャライザーの使い方が継承関係によって変わるのがポイントです。 もしサブサブクラスでも、例えば引数無しの初期化メソッドを必ず用意させたい場合、これはジェネリクスを使って実現することになるんですけど、そういう場合には親クラスの方でそのイニシャライザに required
を付けてあげると、今後ベースクラスを継承した全てのクラスがこの引数無しの初期化メソッドを実装することを義務付けられます。これがあるために、ここが override
ではなく required
になります。これはちょっと特異な言語仕様ですね。required
なイニシャライザをオーバーライドする時には required
というキーワードを使うという、自分の中ではしっくりこないルールがあるんですけど、こうやって required
イニシャライザをオーバーライドします。
このサブサブクラスがコンビニエンスイニシャライザの場合、親クラスのイニシャライザを全部引き継いでいるので何の問題もないんですが、もしこれを指定イニシャライザ(指定初期化子)として実装してしまうと、他のイニシャライザが隠蔽されてしまい、引数無しのイニシャライザが使えなくなってしまいます。ただし、required
が付けられているため、ここで init
がないというコンパイルエラーが発生します。これをよく見かけるのは、最近ではあまりないかもしれないですが、UIViewController
に独自のイニシャライザを追加した時とかに、途端に「このイニシャライザがない」や「init(coder:)
のイニシャライザがない」といったエラーが発生することがあります。この「init(coder:)
がないよ」といった状態がよく言われたりしますね。
イニシャライザを追加することで、他の色々なイニシャライザが使えなくなり、エラーが出るのは、この required
イニシャライザの仕様によるものです。いろいろ話しましたが、大事なポイントとしては、インスタンスを確実に初期化するための仕組みが備わっているということです。これがコンパイルエラーを出さないと、意図しない初期化結果が得られてしまうといった具合になります。コンパイルがエラーを出してくれることで、不自然な初期化構成を検出できるのは嬉しいところですね。
イニシャライザをまだ理解しきれていない人は、クラスの継承を使っていろいろとイニシャライザをいじってみると、なぜエラーが出るのかをじっくり眺めることで、結構勉強になる面白いポイントです。
著者はここで、残り時間が少ないので、前回話しそびれた「このルールを無視するとどうなるのか」という話をしようとしていました。具体的な間違った使い方を実際に見ることはできないのですけど、Objective-Cの力を借りることで興味深いことができるのです。Objective-Cはそのようなチェックが入らないので、面白い現象を起こすことができます。
例えば、ベースクラスをObjective-Cで作るとします。Objective-Cを使うということになりますね。それでは、これで終わりたいと思います。ありがとうございました。
イニシャライザを作っていきますか。プロパティを持たせたいですね。プロパティは例えば atomic
で readonly
でもいいでしょう。何でもいいですが、NSInteger
の value
というプロパティを持たせたとします。ここで持たせなくても良かったのかもしれませんが、とりあえずこんな風に書いてみます。
次に、インスタンスタイプのイニシャライザをこのように書きます。
self = [super init];
if (self) {
_value = 0; // 0初期化
}
return self;
これがObjective-Cのセオリーですね。Bridging Header
に #import
を使います。#include
と #import
の違いは、1回だけのインポートにするか、書いた回数だけインポートするかの違いですね。そんな違いは今どきどうでもいいんですが。
ここでクラスを作るときに、ベースクラスを @interface BaseClass
として作ります。SwiftName
を付けておきます。
NS_SWIFT_NAME(Base)
@interface BaseClass : NSObject
@end
次に、サブクラスを作ります。
@interface SubClass : BaseClass
@end
イニシャライザを複数作りたかったのですが、ここで終わりにしましょう。 だから、これでクラスinit
ウィズAとinit
ウィズBとして話していたんですが、大事なことを言い忘れました。大事なことをちょっと後で言いますね。init
ウィズAとinit
ウィズBを、インスタンスタイプのinit
ウィズBをコンビニエンスイニシャライザ的にして、self = self.init
ウィズAみたいに書きますね。こうやって、ウィズBの方がコンビニエンスイニシャライザで、ウィズAの方がデザインネイトイニシャライザです。
それで大事なことを言い忘れていましたけど、クラスでオーバーライドできるのはデザインネイトイニシャライザだけなんですよ。なのでinit
ウィズAとinit
ウィズBをこうやって書くことはできるんですが、オーバーライドする場合はコンビニエンスイニシャライザは使えません。デザインネイトイニシャライザは親クラスのAを呼び、Aに入れて、親クラスのBを呼びます。だから普通はこういうふうな書き方になります。
オーバーライドするからには、デザインネイトイニシャライザなので親のイニシャライザを呼びます。これがSwiftのクラスだった場合、例えばinit
ウィズAがあって、コンビニエンスinit
としてinit
ウィズBがあったとき、これを例えばObjective-Cで作った場合にはクラスはエラーなくコンパイルできますが、Swiftのクラスで作るとコンビニエンスイニシャライザはオーバーライドできないのでコンパイルエラーになります。
これはイニシャライザが確実に実行されるようにするためで、それをコンパイラがエラーで教えてくれているわけです。Objective-C版のクラスではエラーが検出されずうまくいったとしても、Swift版のクラスではエラーが出ます。
例えば、let x: Int
型の定数x
があったときに、自分自身のプロパティはデザインネイトイニシャライザで初期化しなければなりません。init
Bの中では2を設定するとします。これで定数x
はlet
、そしてこの2つのデザインネイトイニシャライザがあったときに、B
を呼んだ時、x
は何になるでしょうか、という問題です。
print(obj.x)
が何になるか見たときに"1"と出ます。ここが大事なポイントです。B
のイニシャライザを読むと、x
に2が入ります。しかし、親クラスのA
のイニシャライザを呼んでしまうと、定数x
に1が代入されるので、親クラスが初期化されて、結果的には定数let
に値が2回代入されます。
これは極端な例ですが、こういった感じで確実に初期化したはずの値が思ったものと違う結果になってしまうことが起こります。
以上のような問題を防ぐために、クラス継承におけるイニシャライザはきちんと管理されているという話でした。今回はこれで勉強会を終わりにします。