https://www.youtube.com/watch?v=kG_DYC14tJQ
引き続き A Swift Tour
の「オブジェクトとクラス」を見ていきますけれど、今回はその中から サブタイピング
と オーバーライディング
あたりについての特徴を眺めていきますね。この辺りについては前回にも多重継承という観点から見たりしましたけれど、今回はまた違った視点で見ていこうと思いますので、どうぞよろしくお願いします。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #48
00:00 開始 00:49 サブタイピング 01:51 親子の呼び方 07:12 特化 10:48 Swift のクラス事情 12:40 継承は必要に応じて行う 14:34 オーバーライディング 19:14 配列の恩恵 22:55 オブジェクト指向の課題 25:09 配列やオブジェクト指向のありがたみ 27:38 オブジェクト指向に対する安全設計 30:25 メソッドの隠蔽はできない 34:58 オーバーライディングのおさらい 35:45 イニシャライザー 36:36 指定イニシャライザー 37:39 便宜イニシャライザー 40:06 必須イニシャライザー 44:33 オブジェクト指向の難しさ 49:04 クロージング ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #48
はい、じゃあ今日は引き続きオブジェクトとクラスのお話ですけれど、前回は多重継承のあたりで盛り上がりましたね。詳しく自分の知る限りのことを話していきましたが、今回もオブジェクト指向のサブタイピングに注目して、もう少し基本的なところを見ていこうと思います。
前回、このサブタイピングの基本のところをお話しして、ポリモーフィズムで実現されている理屈の一つがサブタイピングだと説明しました。基本的にはシンプルに言うと、クラスを継承するというのがサブタイピングです。基底クラスがあって、そこから派生クラスを作るという感じです。ですが、この説明をする際に困るのが、いろんな呼び方があるという点です。
クラスについて話すとき、基本的にはベースクラスがあってサブクラスがあるというふうに理解されます。ベースクラスを継承するという話ですが、このベースクラスやサブクラスという呼び方以外にも、いくつかの呼び方があります。例えば、規定クラスと派生クラスなどです。
Twiftではどうでしょうかね。多くのオブジェクト指向言語がこれをそう呼んでいます。親クラスや子クラスとも呼ばれますし、そこが自然かもしれません。確かに、親クラス・子クラスという呼び方もありますね。これ、C++でよく聞く気がします。自分が読んだ入門書ではそう呼んでいた記憶があります。基底クラスを親クラス、派生クラスを子クラスですね。英語ではペアレント(親)とチャイルド(子)という表現になります。
なるほど、いろんな呼び方がありますが、どれも知っておくと混乱せずにすみます。そのため、いくつか紹介してみました。親クラス、子クラス、基本クラス、派生クラス、ベースクラス、サブクラスなど、それぞれの言語や書籍によって呼び方が異なります。
スーパークラスについてですが、これは当人から見て親クラスの意味合いが強いので、呼び方に応じて使い分けることになりますね。スーパークラスというと、子クラスがスーパークラスを持っている感じがするので、むしろ親クラスという呼び方のほうが自然でしょう。
こんな感じで言葉がいろいろありますが、オブジェクト指向のポイントはクラスの継承を通じて特化していくということです。基底クラスがあって、そこから新しいクラスを作り、それがさらに特化していく感じ。たとえば、Animal
クラスからCat
クラスといった具合に特化していきます。
クラスを継承する際、機能がどんどん特化していくのがポイントです。例えば、NSObjet
がNSString
になるとか、UILabel
に特化していくような例です。クラスを作り、継承するというのはシンプルですが、クラスの継承は思った以上に複雑です。
Swiftでは簡単な値型を表現するためにクラスはあまり使わず、むしろ構造体(値型)を多用します。これはSwiftの設計方針で、クラスはフレームワーク内だけで用いることが多く、ユーザーが自ら設計することは少ないです。その影響で、クラスの設計や継承に関する知識が少ない開発者もいるかもしれませんね。
今回はこのあたりの複雑なところを含めて、オーバーライディングの話も紹介したいと思います。オブジェクティブCに慣れている人に対する注意事項も含めて説明します。オブジェクティブCでは必ず基底クラス名を指定しなければならなかったが、Swiftではそれが不要になりました。これは小さな変更ですが、紹介しておきます。オブジェクティブCでは必須だった基底クラスを書かなくてもよいのがSwiftの特徴の一つです。 他のオブジェクト指向言語の場合、大体こうだと思うんですけどね。なので、安心して気軽に使えるというのがまず一つ昔の人向けの補足事項です。
次に、オーバーライディングについて説明します。オブジェクト指向でクラス継承が存在するのは、オーバーライディングを使って基本クラスの機能をカスタマイズできるからです。これはオブジェクト指向の大事なポイントです。基本クラスを一つのグループにまとめ、各サブクラスに応じた振る舞いをさせることができる機能を持っています。
簡単な例を挙げると、UIView
というクラスがあります。これが例えば四角形の描画を持っていて、この中で例えば背景色を塗りつぶすメソッドがあるとします。これが UIView
の発想の中で UILabel
というサブクラスを作って、描画方式を変えてテキストを持たせ、そのテキストを表示するメソッドを書きます。このように、背景を塗りつぶすだけではなく、テキストも表示するカスタマイズができるのです。
実際に UIView
を扱うとき、UIView
の配列を作り、その中に例えば UILabel
や単純な背景を持つ UIView
を含めることができます。どんな UIView
でも、UIView
を継承してさえいれば、その配列として一括にまとめることができます。例えば、 viewArray
で view.draw
みたいに、どんなオブジェクトに対しても同じコードで処理できます。これがポリモーフィズム(多態性)やサブタイピングの目的であり、様々なことを簡単なコードで書けるようになったのです。
このあたりは、普通にタッチフレームワークを使っていれば addSubview
で何でもビューを追加できるので、十分恩恵を受けていると思います。
この便利さの発想から、自分が連想して思い浮かぶのは配列です。今の人がどのように勉強するかは分かりませんが、プログラムを書いていて、例えば三つの値を持っていて、その三つそれぞれに対して同じことをする場合、独学の人は配列を使わず、変数を複数用意して計算することがあります。
例えば、カートのアイテムの price1
、price2
、price3
という三つの値から合計を計算する場合、 total = price1 + price2 + price3
のように書いていたとします。しかしカートに入るアイテムが増えれば、この方法では手に負えなくなります。そのとき初めて配列というものを発見し、次のように書ける感動を覚えるのです:
let prices = [price1, price2, price3, ...]
var total = 0
for price in prices {
total += price
}
この感動は、オブジェクト指向でも同様に感じることがあります。配列はシンプルな値版ですが、オブジェクト指向はその機能版といえます。どれだけ多くの機能があっても、一つのコードでまとめられる強力なパワーを持っているのです。
それもあって、オブジェクト指向言語が現代に台頭し、例えば C++ や Java がその代表例です。オブジェクト指向の素晴らしさは、人間の英知の結集ともいえます。配列で分かりやすく説明できますが、オブジェクト指向も同様に素晴らしいのです。 ただ、これが最近の主流としては、「人類には早すぎた」なのか、それとも「人類にとっては威力がありすぎた」のか、そのあたりの事情は分かりません。いささか高度すぎて制御しきれないという面があって、その安全性をどう確保していくかがなかなか難しいところです。
それを解決する手法として、前回お話しした解決手法のひとつとして、特にC++のオブジェクト思考は多重継承などもあって、さらに混沌とした世界が広がっていたのを対象する手立てとして、Javaは多重継承を禁止してインターフェースを導入するなどの工夫がされていました。そして、そのスタイルが主流だったと思います。
Swiftになって、プロトコル思考も恐らくオブジェクト思考の複雑さを睨んで、安全性を確保するための新しい考え方として導入されています。これがプロトコル思考で、値型と相性がとてもよく、安全性が飛躍的に高まるというのが面白い点です。それについては、次の次のセクション「プロトコルとエクステンション」のあたりで紹介しようと思います。
今回はオブジェクト思考にアプローチしたSwiftの安全性の配慮について見ていこうと思います。このテーマで良いコメントをいただいたことがあります。「配列って何?」と聞かれることについてです。自分はCobol言語は知らないので配列があるかどうかは分かりませんが、配列がある言語であっても、最初は配列を知らない人がいることがあります。今時の人は分からないかもしれませんが、配列を知らない人に教える経験は私にも何回かあります。
配列は意外と高度な機能だと思います。オブジェクト思考も嫌われがちですが、分かって使えばとても価値があり、使いどころがある機能です。オブジェクト思考の嬉しいところは、実装をしっかり持てることです。
それで話が飛躍してしまいましたが、オブジェクト思考で実装を持てるというのは、実装があることを確約できるため、この先でプラス形状に味付けする時に楽であったりします。プロトコルでやるよりもね。まあ、それは余談として、オブジェクト思考に触れる機会が最近少ないと思うので、この機会にしっかり触れておいて、Swiftでも使ってみるのは悪くないでしょう。知見を広げていきましょう。
オーバーライディングの大事な機能として、オーバーライドで印を付けることがあります。Swiftではoverride
キーワードを付けることで、親クラスの機能をオーバーライドしたことを明示する仕様になっています。C++では、親クラス側にvirtual
を付けて、子クラス側には特に何も書かない仕様でした。
C++の場合は、クラスの設計者がどの機能をオーバーライドしていいかを制御することが前提になっています。それに対して、Swiftでは逆に、基本的にクラスのメソッドはすべてオーバーライド可能で、オーバーライドさせたくないときには設計者がfinal
を付けます。それにより、オーバーライドか否かを明確にすることができます。たまたま同じ名前を持っている機能と同じ名前を偶然付けてしまった場合の問題を防ぐ仕様になっています。 なので、それを抑制するために「override」というキーワードを付けさせるというのが、まず一つSwiftの大事な仕様になっています。と言っても、そんな大した仕様ではないんですけど、これがまず一つです。
もう一つ大事なポイントとして、「override」を必ず付けなければならない理由があります。同じメソッドが親クラスにあった場合、必ず「override」を付ける必要があるのです。これは親クラスのメソッドを子クラスで隠蔽することがSwiftではできない仕様にするためです。
具体的に言うと、親クラスのメソッドが自分自身のdrawRect
を呼び出していたとします。また、同じコードでなくても、例えば「アクション1」と「アクション2」にしてみましょう。この場合、親クラスのメソッドがself.drawRect
を呼び出していた場合、隠蔽が可能だと仮定すると、親クラスのdrawRect
が実行されることになります。
例えば、UILabel
がUIView
を継承してアクションを持っているとします。このUILabel
でアクション1
を呼んだとき、親クラスのアクションが実行され、self.drawRect
を実行すると、親クラスのdrawRect
が呼ばれる、このような動きになります。
しかし、Swiftではこの隠蔽が許されていません。必ずoverride
をすることが要求される仕様なのです。この仕様により、アクション2
でself.drawRect
を呼んだ場合、この子クラスのdrawRect
が呼ばれます。UILabel
が継承しているアクション1
を呼んだときも、親クラスのアクション1
が実行され、self.drawRect
を呼ぶと、override
先の子クラスのdrawRect
が呼ばれることになります。
つまり、今回の場合、UILabel
側のdrawRect
が呼ばれるので、アクション1
でもアクション2
でも同じ動きを見せることになります。UILabel
からすると、drawRect
と言えば自分自身のdrawRect
を指しますので、自然な動きになるわけです。
override
した先に限っては、super.drawRect
という形で親クラスのdrawRect
を指定することで、親が持っていた振る舞いを継承することが可能です。これがSwiftのオブジェクト指向の大事な設計の一部です。
もう一つ大事なポイントとして挙げられるのは、「イニシャライザー」です。前々回にお話ししたイニシャライザーですが、オブジェクト指向が絡むと非常に複雑な要素を見せます。
素直にイニシャライザーという概念だけを自由に使わせると、オブジェクト指向の初期化が時に破綻することがあります。この問題を改善するためにSwiftでは、何かプロパティを持つオブジェクトがあった場合、そのプロパティを初期化する役目を持っているものを「designated initializer」と位置づけています。 こうやって「Designated Initializer(指定イニシャライザ)」というのがあって、指定イニシャライザは何もキーワードをつけずに init
という書き方です。この指定イニシャライザの大事な役割は、自分自身が持っているプロパティを確実に初期化するという役割を担っています。イニシャライザの基本的な役割ですね、これを持っているのが指定イニシャライザです。
その他に「Convenience Init(便利イニシャライザ)」というのがあって、ちょっとパラメータをつけておきます。指定イニシャライザの他に便利イニシャライザがあって、こちらの場合は自分自身の別のイニシャライザに対して初期化を委ねる(委譲する)という役目を持っています。大事なポイントとしては、例えばセルフのイニシャライザを呼ぶ前にそのパラメータを都合よく加工して、その加工したパラメータをイニシャライザに委譲するという役割があります。
その他の役割としては、指定イニシャライザを呼んでその初期化済みの結果をカスタマイズするというものです。このどちらかの役割を持っているのが便利イニシャライザです。大事なのは完全な初期化は指定イニシャライザに委ね、便利イニシャライザは初期化前のパラメータを加工したり初期化後の自分の値をカスタマイズしたりすることです。これが基本的にこの二つの役割です。
なぜこれが用意されているかというと、こういった役割を分けておかないとオブジェクト思考が破綻することがあるからです。
他にも「Required Initializer(必須イニシャライザ)」というのがあります。Swiftのオブジェクト思考の大事なポイントとして、クラスの継承をしても基本的にプロパティやメソッドが隠蔽されることがないんです。隠蔽されるってどういうことかというと、C++ではできたんですけど、例えばクラスAがあってメソッドがあったときに、クラスAではそれでよかったけど、クラスBではそれを触らせたくない、みたいなことがあったとします。C++ではそのアクションをプライベートとしてオーバーライドすることで、Aからはアクションが使えたけれど、Bからは使えないという状況が作れました。しかし、Swiftではそういうことはしません。親クラスでアクションというメソッドを持ったからには、それを継承したクラスBでもそれを隠蔽しないようにしなければならない。APIが必ずあることが保証されるのです。
しかし、イニシャライザだけはちょっと別です。親クラスにイニシャライザがあったときに、何もしないと子クラスはイニシャライザを継承しますが、子クラスがイニシャライザを実装した途端に親クラスのイニシャライザは隠蔽されます。これはなぜかというと、子クラスには子クラス専用の値が用意されているかもしれません。そのときに、この子クラスの値を初期化することができるのはその子クラスだけなのです。
つまり、親クラスのイニシャライザを呼ばれてしまうと、親クラスのイニシャライザは当然子クラス、その将来継承されるかもしれない子クラスのことまでは知らないわけです。なので、このイニシャライザの中では子クラスの完全な初期化は保証できない。親クラスの指定イニシャライザは子クラスの指定イニシャライザとしては存在できません。ですから、子クラスに特別な初期化が必要ならば、親クラスのイニシャライザではまかないきれず、隠蔽しますという形になります。
そのときに、このインターフェースは隠されては困るんだという場合には required
を付けます。そうすると、親クラスの init
は子クラスで必ずオーバーライドしなければならないという約束が出てきます。これによって、子クラスが正確に初期化できるイニシャライザを持つことができるのです。
この required
イニシャライザは、メタタイプB、例えば let typeA = A.self
や let typeB = B.self
みたいにメタタイプを持たせたときに関連してきます。typeA
が引数なしの init
を持つ場合、typeB
でそれをオーバーライドするには、required
を使うといった細かいルールがあります。しかし、これについては細かいのでここでは詳述しません。 とりあえず、こうするとBもイニットを持つ、みたいな感じになるのですが、これが例えばBもないほうが楽ですかね。今のところちゃんとできていますかね。
こうやってAのメタタイプに対してAのイニットがあり、クラスAがリクワイヤードなイニットを持っていなかった場合、ここでエラーが発生します。そうそう、イニットがなくなると補完が出てこなくなっていますね。これがリクワイヤードイニットが働く大事なポイントです。大事とは言っても、あまり使う機会はないかもしれませんが、このリクワイヤードがあるかないかによって、メタタイプからそれを呼べるかどうかが変わってきます。
たとえリクワイヤードがなくても、アクションはちゃんと出てきます。これはなぜかというと、このタイプAに対して、これがA型のメタタイプになっている時に、仮にクラスBを作ってAから継承させた場合、Bのセルフを入れても、タイプAのイニットがここで出てこなくなります。もう出ないという説明は難しいですが、これが何故出ないのかというと、何もイニシャライザーを実装していない時には、親クラスのイニットが継承されているからです。ただし、ここで別のイニシャライザーを追加してしまうと、親クラスのイニシャライザーが継承されなくなります。
そうすると、メタタイプAがいくらイニットを持っていても、Bのメタタイプが入った時点で、「このイニシャライザーが存在しない」という状況になるのです。これを防ぐために、メタタイプからは、何も指定されていないイニシャライザーは呼べません。ただし、リクワイヤードを付けることにより、継承先でも必ず同じインターフェースを実装する義務が発生します。その結果、メタタイプAに対してイニットが呼べるようになるのです。
こうした制御をしないと、オブジェクト思考はすぐに破綻します。これが難しいところですが、この辺りをしっかり理解していくと、意外とSwiftでオブジェクト思考をきちんと作り上げることができます。逆に、この辺りの話は他のオブジェクト思考言語でも同じです。基本的な考え方が一緒なので、この辺りを理解していなかったということは、オブジェクティブCでも同様に破綻する状況で使っていたことになります。自分もこれをSwiftで初めて知ったのですが、「今までのオブジェクティブCがよく動いていたな」と思うこともあります。
オブジェクト思考を突き詰めるためには、まずSwiftでオブジェクト思考を実践してみるのも良い勉強になると思います。いろいろなところを見ていくことで、オブジェクト思考を面白く使っていけると思います。
時間が少し過ぎてしまったので、今日はこれぐらいにして終わりにしようと思います。お疲れ様でした。ありがとうございました。