https://www.youtube.com/watch?v=IC8g-Z7QVpk
今回は引き続き A Swift Tour
の「オブジェクトとクラス」について眺めていきます。そんな中から、今回はさりげなく大事な「イニシャライザー」についてみていきます。見どころ豊富でどこまで細かくみていくか迷うところではありますけれど、せっかくなのでゆっくりみんなでいろいろ見渡せたらいいなと思ってます。どうぞよろしくお願いしますね。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #45
00:00 開始 00:23 前回のおさらい 01:18 インスタンスの扱い 02:05 ドット構文 03:26 オブジェクト指向の基本 05:57 オブジェクト指向の認知度 07:14 オブジェクト指向の原則 08:48 談笑タイム 09:28 リスコフの置換原則 10:21 言語による理論の違い 11:30 言語によるオブジェクト指向の違い 17:03 仮装テーブル 18:13 ダックタイピング 19:56 プロトコル指向 24:21 AnyObject 31:09 質疑応答 37:31 AnyObject と @objc 41:13 学習コストの低い言語構文 42:24 イニシャライザー 44:02 イニシャライザーの義務 46:19 質疑応答 53:45 次回の展望 ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #45
じゃあ始めていきますね。今日も引き続き「Objects & Classes」についてお話ししていきます。前回はクラスの定義について話しましたが、大事なポイントとしてはプロパティの書き方が変数とまったく同じであることが「売り」ですよ、というところをお話ししました。非常に面白いところですので、前回参加しなかった方はアーカイブを見てもらえるといいかなと思います。本当にとても大きな売りポイントです。
さて、前回の練習問題で「引数を取るメソッドを追加してみましょう」という課題がありましたが、そのときは特に進めずに終わってしまいました。ただ、関数を定義するのと同じ書式でパラメーターを取ることができるというお話なので、今回はとりあえずパスして進みます。時期にまた出てくると思います。
今日のお話はまずインスタンスの扱いです。これは大したことではないですけど、見ていきましょう。とりあえずクラス名に丸括弧を添えることでインスタンスを生成できます。他の言語で言うとイニシャライザーを呼び出す、というイメージですね。つまり、丸括弧はイニシャライザーに渡す引数リストという感じで捉えれば全然問題ありません。
インスタンス化したものを使って、インスタンスのプロパティに対して値を設定したり、インスタンスに対してメソッドを呼び出すときにドット構文を使います。例えば、「何とか.プロパティ」や「何とか.メソッド」といった形です。この仕組みがオブジェクト指向の基礎とされています。今時、ほとんどの一般的な言語はオブジェクト指向を導入していて、ドット構文も導入されていますので、そんなに難しいことではないかと思いますが、一応この辺りをもうちょっと補足していきます。
オブジェクト指向の大事なポイントとしては、クラスを設計図として定義することです。例えば、ある型(クラス)が ID
を Int
型で持つといった設計をして、その設計に基づいて、例えばイニシャライザーとして ID
をパラメーターで取るようにすると、自分自身の ID
に対してパラメーターで受け取った ID
を入れる、といった形になります。これが型を定義するときの一般的な形式です。
型をイニシャライズするときは、型名の後に丸括弧で引数リストを添えます。これで初期化が完了し、その型のインスタンスが生成されます。その後、そのインスタンスを使って動作させることができます。これがオブジェクト指向の基本です。少し言い過ぎましたが、使い方の基本ですね。
オブジェクト指向について話を進めていきますが、皆さんオブジェクト指向はどの程度マスターしているか気になります。自信があるという人もいれば、自信がないという人もいるでしょう。「全く分かっていない」というコメントがありましたが、これがどういう意味か気になるところです。本当に何もわからないということはないと思いますが、雰囲気はわかっていても、それが本当に正しいかどうか自信がないという状態ではないでしょうか。
オブジェクト指向の理論を突き詰めていくと、かなり詳しいところまであります。例えば、オブジェクト指向の原則としては、メッセージパッシングだとか、他にもいくつかの原則があります。3つぐらいですね。 そんなにあるんですか。自分が知っているのは、その継承関係の原則です。詳しいことは忘れてしまいましたが、NSString
と NSMutableString
の継承関係は原則に反しているみたいな話です。確かにこれは、リスコフの置換原則に反しているのではないかと言われています。ですが、ここは詳しいことがわからないのであまり断言できませんが、難しい世界があるようです。
リスコフの置換原則についてですが、基本クラスを使っている場所でサブクラスを使っても問題なく動作しなければならないとされます。しかし、NSMutableString
の場合はどうなのか、少し理解が追いつかないところです。高瀬さんという方が詳しいので、特別講義などを読んでみるのも良いかと思います。スペシャリストが多いので、そういった方々に教えてもらうのも良いでしょう。
自分の持論としては、専門用語やオブジェクト指向の大原則は大事ですが、その言語に特有のオブジェクト指向も理解しなければなりません。最初に携わっている言語のオブジェクト指向をしっかり把握しておけば、一旦は大丈夫だと考えます。ある程度学んでその言語で一定のレベルに達することが重要です。その後で他の言語のオブジェクト指向も見ていけば良いと思います。
他の言語のオブジェクト指向をおさらいしてみると、Objective-C はメッセージパッシング方式を使ったオブジェクト指向です。書き方がいろいろ違っています。例えば、NSString
クラスのインスタンスを作る場合は NSString *str = [[NSString alloc] initWithString:@"example"];
のような形で使います。メッセージをインスタンスに対して送り、例えば、[str uppercaseString]
のように呼びます。
Objective-C では基本的にはドット構文がないのですが、Objective-C 2.0 からはプロパティアクセスに対してドット構文が使えるようになりました。基本的には、大括弧を使ってメッセージを送信し、レシーバーに対してメッセージを送信します。レシーバーがメッセージを受け取れる場合は適切な応答を返し、受け取れなかった場合はエラーを出すなどの処理をします。このようにメッセージのやり取りでオブジェクト指向が実現されているのです。 メッセージを送るというのは基本的にランタイムで行うので、実行時にそのインスタンスが何者であれ、そのメッセージを受け取れるようであれば応答してくれます。受け取れなかった場合には0相当の値を返すという仕組みになっています。これが基本的な動きです。
C++の場合、クラスを定義する際に特性を持たせるために、例えばint
型のバリューを作成し、それをゲッターとして返すメソッドを作成します。例えば、int型
を返すgetA
メソッドを書くといった具合です。メンバー変数はm_
のようにアンダースコアを付けて書くことが一般的です。初期化も同様に行い、初期値を0にしたり、コンストラクターを作成したりします。
インスタンス化する方法は複数あります。オブジェクトに対してイニシャライザーを働きかける方法や、ポインターを使ってnew Object
とする方法などがあります。普通に型として定義したときには.
構文(ドット構文)で書けますが、ポインターにした場合には->
(アロー構文)に変わります。C++の場合、ポインターで扱わないとクラス継承が生かされない仕組みになっているため、その点も特徴です。
C++ではメッセージパッシングではなく、Vテーブル(仮想テーブル)という仕組みを使ってメソッドの位置を管理しています。コンパイルタイムにどのメソッドを呼ぶかを基本的に確定させるので、それによって特徴が出てきます。
一方、Swiftの場合も基本的に仮想テーブル(バーチャルテーブル、Vテーブル)を使ってメソッドを特定するスタイルを取ります。ランタイム時にメッセージを投げるということをしないため、その分動作速度が速くなるメリットがあります。逆に、実行時にどんなインスタンスであってもメッセージを投げることがしにくくなるので、ダックタイピングのような仕組みは使いにくくなります。
ただし、Swiftにはダックタイピングに似た仕組みが用意されています。例えば、プロトコルを使って実行時に適切なメソッドを呼び出すことができます。プロトコルを使って宣言した関数を、プロトコルに準拠したクラスが持っていれば、その関数を呼び出すことができます。
protocol P {
func X()
}
class A: P {
func X() {
print("A's X")
}
}
class C: P {
func X() {
print("C's X")
}
}
var obj: P
obj = A()
obj.X() // "A's X"
obj = C()
obj.X() // "C's X"
このように、プロトコルを使えば、実行時にインスタンスに応じて適切なメソッドを呼び出すことが可能です。これはダックタイピングには及ばないものの、動的にメソッドを呼び出すという点では似たような仕組みと言えるでしょう。 ただ、これが面白いところです。これをプロトコルの回に持っていった方がいいかもしれませんが、ざっくりと紹介しておきます。例えば、プロトコルPがデフォルト実装としてprintP()
というメソッドを持っていたとします。このとき、クラスCのインスタンスからX
を実行すると、通常はprintX
が出力されます。しかし、エントリーポイントやカスタマイズポイントを考慮しない場合は、printX
として出力されるはずのものがprintP
に変わることがあります。
こういうふうに、Vテーブル的な呼び出し解決、つまりコンパイルタイムに解決する仕組みが伺えます。この辺りは、いずれまた詳しくお話したいと思います。このように、動的呼び出しと静的呼び出しがSwiftでは混在している感じになっています。
いろいろお話ししましたが、これは全然オブジェクト指向の話ではなく、プロトコル指向の話です。これはまた追々お話しするとして、オブジェクト指向の話に戻ります。もう一つ、とても面白いダックタイピング的な呼び出し手法があります。これはObjective-Cランタイムを使う方法です。
例えば、Identifier
型が@objcMembers
を使っている場合、Identifier
型のメンバーはすべてObjective-C準拠ですよという宣言をします。例えば、let object = Identifier()
と定義したとき、object
をAnyObject
型として定義します。こうした場合AnyObject
に対してメソッドを呼び出すことができますが、Foundation
をインポートしないと@objc
が使えないという仕様になっています。Foundation
とObjective-Cに直接の関係はない気がしますが、とりあえず、AnyObject
型として定義したものに対してdot notation
でメソッドを指定することができます。
例えばobject?.length
のように呼び出すと、nil
が返ってきます。これは、Identifier
型がそのメッセージに応答できないためです。Objective-C的な動きをSwiftでも再現してくれるのがAnyObject
型の面白い特徴です。
この場合、例えばNSString
型のインスタンスに置き換えると、length
が1として返されます。また、メソッドの存在を確認して呼び出す場合には、カテナ(?
)を使って、「このメソッドが存在しない場合はnil
にしてください」という表現を行います。
実際に書いてみるとわかりますが、メッソド呼び出しにビックリマーク(!
)を付けるとランタイムエラーが発生しやすくなります。そのため、if let
やguard let
を使って、メソッドが存在するかどうかを確認しながら安全に呼び出すことが重要です。
if let response = object?.uppercased() {
print("Success: \\(response)")
} else {
print("Failure: Method not found")
}
とすると、成功したときにSuccess
と表示されます。これにより、安全にメソッドを呼び出すことができます。このように、言語仕様として若干使いにくい部分もありますが、慎重に扱うことでエラーを回避することができます。 とりあえず、面白いのが AnyObject
です。ちなみに、メソッドじゃなくていいや。この id
を取ってみましょう。オブジェクトに対して id
を取得します。id
は色々な言語にあるからややこしいところですが、今回はオブジェクト型なので大丈夫そうですね。
id
はオブジェクト型の設計の時には Int
になっていたんですけど、AnyObject
から呼ぼうとすると、ちゃんとオプショナル型に勝手になってくれるんです。例えば、NSString
みたいにid
を持っていないものに対して" id
ありますか?”と問いかけると、nil
がちゃんと返ってくるという動きを見せてくれます。
ちょっとプレイグラウンドが大人しくなっちゃってますけど、動くのを待ちつつコメント拾っていきましょうか。あとは、.x
と書いてあるのがメソッドよりプロパティっぽい書き方ですね。確かにそうです。内部的にもしかするとプロパティが関数型を持っていて、それがオプショナル型になっているみたいな雰囲気で捉えられているのかもしれません。
あと、戻り値が nil
の場合、ちょっと複雑な話で諦めましたが、なるほど。戻り値が nil
の場合、実装があるかどうかの実装と「はてな」で値を取る、わかりにくいですね。説明すると、クエスチョンマークが付くものは実装がオプショナルになっています。つまり、実装しなくてもいいという制約です。そのため、使う側は実装しているかどうかのチェックをしてもいいし、クエスチョンマークを使ってチェックするのも良い方法です。
しかし、実装しているけれども nil
を返す場合と、そもそも実装していない場合は異なる動きになります。例えば、ある SDK を使うときに、そういうところでハマりました。戻り値が nil
を返す場合はオプショナルが二重ラップされるとか、そういうお話です。
ここで、具体的なコード例を見ないと難しそうなケースに関して話してみます。例えば、x
がnil
かどうかをチェックする場合、x != nil
なら実装されているし、オブジェクト x?
だけだとプロパティの方が分かりやすいかもしれません。
以下のようにコードを書いてみます。
var v: Int? = nil
こうするとどうなるか。少し自信がなくなってきた。==
や!= nil
で確認してみましょう。実際にやってみると、もし AnyObject
にメンバーがなかったりオプショナル型だから動かない場合もありますね。それでは、次にいってみましょう。
オブジェクト型で NSString
のオプショナルだったら正常に扱えるかもしれません。イントのオプショナル型がちょっと良くなかったみたいです。オブジェクティブシーランタイムを回避する都合でちょっとダメだったのでしょう。
ここで、リロードしてやり直してみると、nil
を返しているのが良くないですね。たぶんイメージと違う結果になるでしょう。おそらく、!= nil
の場合は「インプリメンティッド」と表示されて、== nil
の場合は「No Value」と表示されるでしょう。
困ったときは、そのメソッドを使います。メソッドなら、引数リストがあるかどうかで明確に話ができます。少し AnyObject
だと Swift の言語構文と相性が悪いかもしれませんが、こんな感じで AnyObject
に関する面白いところを紹介しました。AnyObject
の後にドットを付けると、オブジェクトシーメソッド全部が出てくるという面白い現象が見えます。 なので、全然違うクラスを定義して @objc
を付けてあげれば、 AnyObject
の型として扱えるようになります。Swiftではこのような特殊な配慮がされている型は珍しく、非常に面白いので、ぜひ使ってみてください。
respondsToSelector
メソッドも存在します。これは NSObject
が持っているメソッドを呼び出せるけれども、動かないこともあります。これについては詳細は省略しますが、例えば「object.X
なら動きます」といったことが理解できると思います。プレイグラウンドが現在不調なので試せませんが、 respondsToSelector
は NSObject
に存在するので、今回のポイントとしては NSObject
を継承していないと動作しない場合があるということです。
このように、Swiftネイティブなクラスであっても @objc
を付ければ AnyObject
として扱えるのが非常に面白いポイントです。かつては NSObject
を継承しなければならなかった仕様が緩和され、より使いやすくなっています。この特性を活用することで、テストコードの記述なども効率的になるかもしれません。
ここから次の話題に移りますが、言語仕様として .構文
は今時のプログラミング言語では一般的です。JavaやJavaScript、Rubyなどオブジェクト指向言語では普通に使用します。そして、この学習コストが低い点が非常に嬉しいポイントです。Objective-Cではブラケットを使う構文が敬遠されがちでしたが、Swiftの .構文
になれば簡単に書けるようになります。このシンプルな変更によって、使いやすさが大幅に向上しました。
次に、イニシャライザーについて話します。イニシャライザーは init
というキーワードを使用して定義します。パラメータを取り、プロパティを初期化する方法です。Swiftでは、どの型に対してもクラス名を書かずにイニシャライザーを定義できます。イニシャライザーの重要な特徴として、init
メソッドが自分自身のプロパティを完全に初期化する義務を負うという点があります。他の言語にも同様の原則がありますが、例えばC++では初期化されなくてもコンパイルが通る場合があります。Objective-Cにも初期化されていなければゼロ相当の値で初期化される特徴がありますが、Swiftでは明示的に初期化されていないとエラーが発生します。
プログラマーにとってこれは初期化ミスを防ぐメリットがあります。一方で、イニシャライザーを使用した後にはすべてのプロパティが適切に初期化されていることが保証されます。適切な初期化がなされない場合にはコンパイラーがエラーを返す仕組みとなっています。
このように、Swiftのイニシャライザーの特徴とその重要性について理解しておくと、より安全で確実なプログラミングが可能となります。 あくまでも値に矛盾がないかというところまではプログラマーが責任を負わないといけないんですが、値に矛盾が出る可能性のある実行パスがある時にはエラーが出るという仕組みになっています。この辺りも面白いのでお話ししていきたいと思うのですが、その前にZoomのコメントが気になりました。興味深いところとして「new」とは違う動きな気がするという点がとても気になりました。
「new」というのは「new演算子」のことですかね。でも、Javaには確か「new演算子」はなかったような気がします。C++にはありますが、違うのかな? なんだろう。Javaとかだとイニシャライザーを使うわけではなくて「new演算子」とかを使って生成すると思うんですけど、それの動きとイニシャライザーの動きが、なんとなくちょっと違うような気がするなと思って。
なるほど、Javaはどういう書き方をしたかを自分は忘れてしまいましたが、コンストラクタはクラス名で定義してましたね。そうですね、クラス名で定義して、パラメータを取ってフィールドを int
型とかで持っていたとしたら、これを this.field = value;
みたいにするんですよね。Javaクラスのインスタンスを作成する時には必ず new クラス名(パラメータ)
っていう感じですか。そうですね、どこが違うというふうに感じたのか、こう書いてみると結構似ている感じがしたので、どの辺になんとなく違いを感じたかというのがとても興味深いです。
Javaの場合、必ずクラスインスタンスを作るときに new
を使ってインスタンス化するというような、イニシャライザー的な動きを必ずするんですけど、Swiftみたいに「こうしなければエラーが出ますよ」という内部的な許容範囲の振り分けが、Javaの場合にはもう少し緩やかだった気がします。なるほど、確かにこの辺りの定義の違いで若干違いが出てくるかなという気がしていて。
Swift以外の言語だと、初期化に対して甘いルールが多いんです。これはオブジェクティブCでも言えることで、例えばクラスでイニシャライザーが複数あるときに、一つのイニシャライザーを呼ばないケースがあっても、許容されてしまう場合があります。
Swiftのクラスの場合を具体的に考えてみましょう。Swiftではイニシャライザーを init
として定義して、以下のようにします。
class MyClass {
var value: Int
init(value: Int) {
self.value = value
}
convenience init() {
self.init(value: 0)
}
}
大事なポイントとして、自分自身のイニシャライザーを呼ぶときには、クラス継承も考慮すると複雑なことが起こります。流れとしては、自分自身を完璧に初期化する役割を持ったイニシャライザーと、そのイニシャライザーへ処理をつなぐ便宜的なイニシャライザーとを明確に役割分けしておかないと、初期化が矛盾することがあります。特に継承の場合にこの問題が顕著になります。
Swiftにはディジネイティブイニシャライザー(指定イニシャライザー)とコンビニエンスイニシャライザー(便宜的イニシャライザー)の区別があり、これにより初期化の矛盾を防ぐ仕組みが提供されています。他の言語ではこのように役割を明確に規定していないことが多いのです。このため、オーバーライドしたときに初期化が狂う可能性が潜在的に存在します。
時間になってしまったので、次回このあたりを詳しく見ていきましょう。他にもいろいろな配慮があり、さまざまなイニシャライザーにおける安全を考慮する仕組みを次回にまたフォーカスを当てて見ていきます。お疲れ様でした。