https://www.youtube.com/watch?v=S_5G2rpV-CE
今回も引き続き A Swift Tour
の「オブジェクトとクラス」を見ているところですけれど、そんな中でも今回は、前回の最後に話題に登った イニシャライザー
に備わっている仕組み的なところをもう少し詳しく眺めていこうと思います。クラスの話だけに留まらず、イニシャライザーの役割そのものに目を向けてみようと思ってますので、どうぞよろしくお願いしますね。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #46
00:00 開始 02:01 イニシャライザーの存在意義 03:00 イニシャライザーの基本 04:22 シャドーイングと名前空間 07:47 イニシャライザーの引数リスト 07:57 イニシャライザーの大原則 10:11 イニシャライザーの役割 11:12 初期化 16:21 Construction 18:22 Swift での初期化表現 22:46 コンストラクターという言葉 25:51 ドメインによる言葉の違い 27:42 NSProxy 32:10 Java のイニシャライザー 36:08 初期化フェーズ 39:22 プロパティーの二重初期化 42:16 初期化のカスタマイズ 45:29 次回の展望 ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #46
はい、ではイニシャライザーの話に入っていきましょう。
現在のセクションでは、オブジェクトとクラスについてお話ししています。その中で、前回はイニシャライザーについて触れましたが、Swiftのイニシャライザーは他の言語と比べて独特で、安全性を重視した機能になっています。前回はその点にフォーカスしましたね。
今回もオブジェクトとクラスに焦点を当てつつ、特にイニシャライザーについて詳しく説明していこうと思います。具体的な話は後になってから追いかけることもありますが、ここで一度取り上げた方がわかりやすいと考えました。
では、前回のおさらいから始めます。Swiftではイニシャライザーをinit
を使って定義します。他の言語ではクラス名をそのまま使ったり、コンストラクター
という用語を使うこともありますが、Swiftでは安全性を意識してinit
を使うことで統一されています。
一例を挙げると、以下のようなコードが考えられます。
init(name: String) {
self.name = name
}
ここでself.name
とname
がシャドーイングされています。シャドーイングとは、異なるブロック内で同じ名前の変数が存在する場合に一番近いブロックの変数が優先される現象です。Swiftではself
を使うことで、クラスのインスタンスのプロパティにアクセスできます。ちなみに、型変数にアクセスする場合は大文字のSelf
を使います。
イニシャライザーでは、すべてのプロパティに初期値を割り当てるか、またはイニシャライザー内で値を割り当てる必要があります。これが重要なポイントです。
さらに深く理解するために、このあたりのルールや概念をしっかり押さえておくと良いでしょう。では、引き続きイニシャライザーについて見ていきましょう。 ここが当たり前なんですが、イニシャライザーにとっては大原則です。これによって、イニシャライザーの仕様が人によっては複雑化されていると捉えられても、大丈夫かなと思います。ただ、「複雑化」というのはちょっと語弊があって、イニシャライザー自体が複雑なんです。他の言語、例えばObjective-Cの場合でもそうですし、Swiftの場合でもそうですが、イニシャライザーは複雑な仕組みです。
Objective-Cの頃には何気なく使えていました。Objective-Cをやっていない人もいるかもしれないので、C++でもいいですし、Javaでもいいです。その辺りの言語では、イニシャライザーを何気なく使っていました。しかし、Swiftになると、何気なく使ったときにコンパイルエラーが出ることがあります。
この辺りは、Swiftのイニシャライザーが特殊なのではなく、イニシャライザーを何気なく使っておかしい使い方をしないようにガイドしてくれているので、エラーが出るということです。これがとても大事なんです。なぜエラーが出るのかという理由が、今お話しした「全てのプロパティには宣言時またはイニシャライザー内で値を割り当てる必要がある」ということに帰着します。
大事なポイントとして、イニシャライザーの役割といったら複雑なんですが、最終的にはこの「全てのプロパティには宣言時またはイニシャライザー内で値を割り当てる必要がある」ことに行き着きます。必ず。なので、ここを押さえておくと、イニシャライザーがだいぶ簡単に使えるようになってきます。矛盾なく安全に使えるようになっていくわけです。
混乱したときには、このポイントを思い出しましょうというのが大事なポイントになってきます。取り拒絶したくなるぐらいイニシャライザーは主要なものであり、イニシャライザーはそれだけ重要な役割を持っているという話です。この辺りが面白いわけです。
これで前回のお話の続きに移れるかなと思いますが、その前に「イニシャライズとは何でしょう?」という話から始めます。イニシャライズとは、例えばオブジェクト指向とか全然抜きに考えると、あるメモリー領域に意図した値を設定することです。
例えば、var flag = false
これでフラグを false
の状態でイニシャライズしました。Swiftの場合、これだけです。イニシャライザーが走って的確な初期化が行われているというコードになっているところも面白いところなんですが、そのうちこれは感覚的に捉えられるようになるといいかなと思います。
初めは意外と複雑なので、分かんなければ 「変数 flag
に false
が入っている」 ぐらいでひとまずOKです。次のステップに進みたい場合には、これがどういう仕組みで動いているのかを理解していくのが大事になってくる気がします。
こうやって初期化が行われているわけですが、もうちょっと前の時代だと、例えばC言語では int
型に int value = 0
と書いて、ゼロで初期化してますよね。これを昔のアセンブラ、Z80のことで説明します。int
型のサイズは64ビット、つまり8バイトです。アセンブラのコードを書くときはニーモニックを使いますが、その書き方はもう忘れてしまったので、ここからはC言語で書きます。
昔的な発想だと、あるメモリー空間に int
型のポインターがあって、そこに mmap
システムコールを使って例えば8バイト初期化します。そして、memcpy
を使ってポインター p
をゼロで8バイト埋める。memcpy
のパラメータ順序は確かです。間違えました、memset
ですね。これでポインターからゼロで8バイト初期化します。
このように、あらかじめメモリー領域が確保され、その確保された領域を意図したデータで埋めておく。これがイニシャライズ、便宜上ちょっと言いますけど初期化のメモリー領域ですね。 なので用語の違いがあっても、やっていることは基本的に同じです。メモリーを確保し、初期値を設定する流れはどの言語でも変わりません。これがプログラミング言語の初期化の根本概念です。
さて、Swiftではどうでしょうか。Swiftにはプロパティの初期化が非常に厳格です。たとえば、クラスや構造体を定義したとき、全てのプロパティを初期化しなければなりません。これがイニシャライザー(init
メソッド)の役割です。init
メソッド内で全てのプロパティが初期化されていない場合、コンパイルエラーが発生します。
例えば次のように書きます:
class MyClass {
var x: Int
init(x: Int) {
self.x = x
}
}
このコードでは、x
プロパティが初期化されるのでコンパイルが通ります。しかし、もしself.x
の初期化を忘れると、以下のようにエラーが発生します:
class MyClass {
var x: Int
init() {
// self.xの初期化を忘れた場合
}
}
このような場合、"Property 'self.x' not initialized"というエラーメッセージが表示されます。
このようにSwiftは、初期化に対して非常に厳格で、それが開発者がミスを防ぐ助けとなるのです。他の言語、例えばC++では、コンパイラーが初期化を監視しないため、プログラマーが自分で初期化を確保しなければなりません。この違いが、言語ごとの特性といえますね。
そしてこれがSwiftの勉強会で覚えておくべき重要なポイントの一つです。この初期化ルールを徹底することで、コードの安全性と安定性が高まります。したがって、初期化の流れをしっかりと理解し、各プロパティが正しく初期化されているかを常に確認することが必要です。
また、PythonやJavaScriptなどの他の高水準言語でも、同様の初期化の概念がありますが、それぞれが少しずつ異なる特性を持っています。これらの違いを理解することが、マルチプラットフォームでの開発において非常に有益です。
今回の勉強会では、Swiftの初期化と他の言語との比較を通じて、この初期化の概念を深く理解していただけると幸いです。質問がある場合は、ぜひ具体的なコード例を持ってきていただけると、さらに具体的なアドバイスができると思います。 なので、このコンピューティングの常識に当てはめてしまうと、アロケーションが必要そうに見えるんですけど、そんなことはないという風に、ある種の方言コンストラクターと言っているのは、他にもJavaScriptです。JavaScriptもコンストラクターだったんじゃないかな。クラス(JavaScriptクラスみたいなときのコンストラクター)じゃなかったかな、確か。という風に、ずばり「コンストラクター」という言葉でイニシャライザーを定義したりします。
Javaでも、このイニシャライザー、Swiftでいうイニシャライザーに相当する機能のことをコンストラクターと言います。一般的には、C++がコンストラクターと言い始めたからなのかな。他のC系の言語のオブジェクト指向の多くは、イニシャライズする役割のことをコンストラクターと呼ぶと思います。
オブジェクティブCでは、ちゃんとalloc
とinit
といったように、イニシャライゼーションをイニシャライザーとして明確に使っています。その系統のSwiftも、オブジェクティブC系とは言わないですが、そんな立ち位置にいるSwiftはイニシャライザーと呼ぶようです。なので、このあたりは使う言葉が言語によって違うというところを基本として押さえておくのは、結構大事なポイントだと思います。
一般理論ではこの言葉を使っているけれど、言語では違う言葉を使っているというのは、イニシャライザー以外でも往々にしてあるはずです。そのときに、例えば今回の例でC++を見て、「いや、それはコンストラクターではない、イニシャライザーなんだ」といったところでどうでもいい話なんですよ。最終的には、理論は理論。要はドメインによって言葉は変わるよ、というのは頭に入れておくと、いつか役に立つ気がします。
オブジェクティブCからSwiftに移ったときにも、違いが出てきたときに「オブジェクティブCはこうなのに、なぜSwiftはこうなのだ」と考えてしまうと、オブジェクティブCに引きずられて足を引っ張られてしまう。そんなふうに、言葉を捉えるのも大事なところなんです。どこまでしっかりと原則にのっとっていくかというのは大事ではありますが、気にしすぎないように、その塩梅をうまく意識して理解することに慣れておくと良いことがあると思います。
余談になりますが、面白いのが複数ありまして、二つ思い浮かぶところがあります。まず、イニシャライゼーションやコンストラクションといった場合に、アロケーションとイニシャライゼーションがセットになっているという話をしました。そうすると、プロパティ値を持つとき、イニシャライザーでインスタンス化するときに、アロケーションとイニシャライゼーションをセットで必ずやるようなイメージを持ちますが、それは必ずしもアロケーションが必要ないという場面があったりします。
イニシャライゼーションも必要ない場面があります。プロパティを持たない場合、イニシャライゼーションはいらないわけですよね。なくてもいい、あってもいいけど。そんな感じで、コンストラクションといっても、必要に応じてアロケーションし、必要に応じてイニシャライゼーションする。大抵の場合、アロケーションもイニシャライゼーションも必要になります。それはなぜかというと、インスタンスを作ることが絶対に必要だからです。
オブジェクティブCでは、それがとてもわかりやすいです。あるクラスがあったときに、まずalloc
メソッドを呼びます。そして、init
メソッドを呼ぶ。厳密には、NSObject
がalloc
を持っているという仕組みになっています。オブジェクティブC言語は、原則すべてのクラスがNSObject
を継承しているという大原則があります。なので、すべてのオブジェクトはalloc
して入れ物を作り、その後init
をしていくという流れになります。
オブジェクティブCで唯一、NSObject
を継承していないクラスというものがあります。それがNSProxy
クラスです。これはアロケーションを持たないクラスです。インターフェース的なクラスになっていて、メモリを確保する機能は必要ありません。アロケーションが既に終わったものをNSProxy
が扱うため、いきなりinit
するような感じです。キャストするのかなどうかは、自分も使ったことがないので詳しくはわかりませんが、そういった感じでalloc
が使えない、持っていないというケースもあるという話です。
Swiftでは、アロックやそれに関する話は出てきません。まぁ、いいでしょう。 まずは、アロケーションとイニシャライゼーションについてです。必要なものだけを扱えば良いのですが、理屈としては大事な部分です。
ここで面白い話なのですが、Swiftのコンストラクターの構造についてです。11行目で語っているこの話を調べていた時に見つけたのですが、自分の知識として正しいかどうか確信はありませんが、Javaにはコンストラクタとイニシャライザの両方が存在します。これがとても興味深かったので、紹介しようと思います。
具体的に、Javaではまずコンストラクタがあります。その中で初期化を行いますが、他にイニシャライザも定義できます。例えば、フィールドを初期化する役割があります。なので、イニシャライザではまずプロパティを初期化します。例えば、
var x = 0
var y = 0
のように書きます。そのため、イニシャライザとコンストラクタがJavaでは区別されています。
さて、大事なポイントですが、フィールドを初期化するのはイニシャライザの役目です。例えば、13行目から16行目のような書き方です。JavaScriptではコンストラクタがフィールドを初期化しますので、イニシャライザとみなせます。ただ、言語や文化によって概念が異なるので、SwiftとJavaのイニシャライザやコンストラクタは厳密に区別されると混乱することがあります。
C++の場合、イニシャライザという名前のコンセプトがないので、コンストラクタに統一されます。言語によってこのような違いがあるのは面白いと感じます。同じような事をSwiftでも応用することが可能です。
Swiftではプロパティの宣言の時に初期値を設定できます。自分の感覚では、これはJavaのイニシャライザに近いです。Swiftのイニシャライザ、いわゆるinit
メソッドですが、この中で全ての初期化が終わっていれば問題ありません。例えば、10行目で初期化された値を15行目で再度初期化することも問題ありません。
また、イニシャライザを終了する時点までにプロパティが初期化されていなければならないというポイントがあります。プロパティの宣言時に1回初期化の機会があり、イニシャライザの中でもう1回初期化の機会が与えられます。この二つを合わせてイニシャライズフェーズと呼びます。このフェーズを意識しておくことは大事です。
例えば、SwiftのdidSet
ですが、初期化フェーズ以外で値が設定された時に限り実行されます。初期化フェーズの中でいくら値を書き込んでも、didSet
は実行されないのです。これを理解していると、将来的に繊細なコードが書けるようになります。
イニシャライゼーションフェーズの中でのプロパティの初期化方法には、宣言時に初期化する方法と、イニシャライザの中で初期化する方法があります。先にプロパティの宣言で初期化され、その後イニシャライザで初期化される特徴があります。 なので、このように画面に表示中のイニシャライザーを書いたとき、通常のイニシャライザーが実行された後に呼ばれるのです。今回の例でいうと、x
に2回値を設定していることになります。
これが望ましいかどうかはケースバイケースですが、一回の初期化で済ませるべき場面でこのような書き方をしてしまうと、後々問題が発生する可能性があります。特に、初期の段階でうまくいっていたとしても、将来的に型を拡張し、新たなイニシャライザーを定義したときに困ることがあります。例えば、x
の初期値を10にしたいと思ったときに、最初に0を入れなければならない処理が必要になります。これは、効率がよくない印象を与えます。しかし、見た目はスッキリしますね。
このような場合、イニシャライザーで一度だけ初期化することで、異なる値を一度だけ設定するということが可能になります。もしこれが構造体であれば問題は少ないのですが、クラスのように継承可能なものになると、見えないところで2回初期化が走ることもあります。これらを意識してコードを書くことが大切です。
初期化が2回されることが必ずしも悪いことではありません。効率の面では悪いのですが、ソフトウェアをプログラミングする上では、それは必ずしも避けるべきことではありません。例えば、自分自身の別のイニシャライザーを呼ぶときに、convenience
イニシャライザーを使います。この場合、self
のイニシャライザーを呼び出し、その後でx
の値を変更するというコードを書くことができます。
init(value: Int) {
self.init()
self.x = value
}
このような流れになります。まずself
のイニシャライザーが呼ばれて、次にx
とy
が0に初期化されます。その後、このイニシャライザーが終了し、続きの処理が走ります。23行目でx
に改めてvalue
を代入するという流れです。
この例では分かりにくいかもしれませんが、場面によってはこのようなコードの流れが適切な場合もあります。大切なのは、x
の値を再代入したという事実ですが、Swiftではこれを「カスタマイズした」と捉えます。まずデフォルトのイニシャライザーで初期化し、次にカスタマイズしたという感覚です。この手順が完了して初めて、「初期化が完了した」となります。
今日は時間が迫ってきましたが、イニシャライザーの安全性をもっと詳しく見る予定でした。クラスの継承関係を含むとさらに複雑な要素が出てきます。convenience
イニシャライザーと何も書いていないデフォルトのイニシャライザーが関わってきます。
今日の内容はここまでですが、次回(もしくは次々回)にはクラス継承に関連する話が出てくるので、そのときに補足したいと思います。
本日はこれで終わりにします。お疲れ様でした。ありがとうございました。