https://youtu.be/oBTwRi3TZ1k
今回はまず、前回に上手く説明できなくて分かりにくかった気がする プロトコルを型として扱う
ときの 自身への準拠性
について改めて紹介してみることにしますね。
前回のおはなしを思い出しつつ、今に眺めていっている公式ドキュメントの少し先に登場する Language Guide
の Protocol
を確認したりしながら、プロトコル自身への準拠性について整理していきます。それが終わったら引き続き A Swift Tour
の次の項へと進んでいこうと思ってます。どうぞよろしくお願いしますね。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #67
00:00 開始 00:10 今回の展望 00:56 今回の題材 01:27 プロトコル型の特徴 03:02 プロトコルを型として使う 04:10 プロトコル型はインスタンス化できない 05:33 プロトコル型 05:57 余談 07:11 プロトコル型と具象型との特徴の違い 09:38 存在型 11:46 それ自身がそのプロトコルに適合するような型と呼べる? 12:40 プロトコル型から静的メンバーを呼び出せない 13:55 プロトコルに既定の実装を定義したとき 16:43 プロトコル型のプロトコル適合性 17:28 プロトコル型をとる関数には渡せる 18:22 ジェネリクスでとる関数には渡せない 19:29 存在型という言葉が掴みにくい印象 20:38 ここまでのおさらい 20:58 プロトコルを型として使える場面 21:38 イニシャライザーの戻り値の型としては使えないはず 26:35 ほとんどの場面でプロトコル型を使用可能 27:28 プロトコルの命名規則 28:16 存在型の自己準拠 28:21 余談 29:01 プロトコル型はそれ自身には準拠しない 30:29 プロトコルの静的メソッドを利用する方法 33:15 自身に自動準拠するプロトコル型 33:48 Error プロトコルの自己準拠 36:32 @objc プロトコルの自己準拠 38:46 @objc プロトコルに静的メンバーを持たせた場合 40:46 メタタイプ経由で静的メンバーを呼び出す方法 44:05 ジェネリクスの制約の書き方について 45:37 制約がないのと Any は等価 47:34 次回の展望 ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #67
はい、じゃあ、今日は前回、プロトコルを型として扱った時の説明がうまくできなかったなという感覚が残っているのと、意外と面白いところがたくさんあるので、そのあたりを復習がてら見ていこうかなと思います。前回の話を思い出せる人は思い出しながら聞いてもらうと、より理解が深まると思いますし、前回参加していなかったり、すっかり忘れてしまった人も問題なく聞いていけるかなと思います。
では、始めていこうと思います。プロトコルを型として扱うという話についてです。これが『The Swift Programming Language』のランゲージガイド、プロトコルのセクションに書かれている内容です。まずはそこを見ていこうかなと思います。
そこに記載されていることとして、プロトコル自身には機能が実装されていないと。最近のイメージだとこのあたりがうまく納得できない人もいるかもしれませんが、基本的にはこの通りです。プロトコル自身には機能というものは実装されていません。しかし、Swiftの特徴の一つとして、他の言語で考えた場合にインターフェースとして扱う概念、例えばJavaなどと同じように、プロトコル自身も型として使うことが可能です。
例えば、プロトコルで FullyNamed
という形のものがあり、それがプロパティをインターフェースとして備えている、要は fqdn
というプロパティがあり、それが値を実際に持つわけではないけど API としてゲッターが備わっているという状態になっている、という宣言がされています。
つまり、プロトコル自身に機能が搭載されていない状態で定義されているけれど、それを型として普通に使えるということです。実際にどのように使うか、軽く見ていこうと思います。プロトコルで FullyNamed
というものを定義し、これに fqdn: String
というゲッターを設けることで、変数をプロトコルの型で規定することができます。
ただし、プロトコルで型を規定できたとしても、そこにインスタンスを入れる際には注意が必要です。例えば、プロトコルのインスタンスを直接入れようとするとエラーになり、「イニシャライザーがありません」というエラーが出ます。プロトコル型そのものにイニシャライザーを規定できるわけではなく、ここでプロトコル型はインスタンス化できない、つまり必ず具体的な型(具象型)に適合させて使う必要があります。
この場合、 FullyNamed
型に準拠した具象型のインスタンスは FullyNamed
型として使える、ということです。例えば、具象型の方にイニシャライザーがあり、そのインスタンスを FullyNamed
型として扱うことで、API規定されているプロパティを使用することができます。
ここまでがプロトコルを型として使う話の概要です。その際、プロトコル型にイニシャライザーなどを定義していなくても、具象型の方にイニシャライザーがあれば、プロトコル型として振る舞うことができる、ということです。
Swift の用語として、時折「プロトコル型」という言葉が出てきます。自分がこの言葉を使った時には、この内容を指しています。他にも「存在型(Existential Type)」という概念がありますが、これもプロトコルを型として扱う時の用語の一つです。ただ、この存在型は他の言語の型システムにも関連する概念で、少し難しい話になります。
今回の話はこの辺にして、次回またさらに掘り下げていくところがあれば話そうかなと思います。 存在型というのはこういうものだ、という概念までは把握できていないのですが、とりあえず Swift ではプロトコル型を存在型と呼ぶことがある、というところで捉えておくといいみたいです。この存在型が何でそう呼ばれているかということまで書いてあって、それ自身がそのプロトコルに適合する形が存在する、という言葉に由来するようです。要は「存在する」という意味で使うものみたいです。
たとえば、FullyNamed
というプロトコルだったとすると、そのFullyNamed
自身がそのプロトコルFullyNamed
に適合する形が存在する、というふうな表現です。しかし、さっきイニシャライザーが使えなかったという話がありました。プレイグラウンドに戻ると、イニシャライザーが4行目に指定されているのに、14行目ではそれが使えないというエラーが出ています。このエラーは「インスタンス化できない」ということです。
ここで、たとえばスタティックなメソッドを定義して、そのメソッドを型に持たせたとしたとします。このとき、バリュー型ではそのメソッドを呼べるけれど、プロトコル型では呼べないという状況が起きます。たとえば、value
型がエラーになっていないけれど、FullyNamed
型はsomething
というメソッドを呼べない。これはスタティックメンバーsomething
をFullyNamed
プロトコルが持っていないからで、バリューはタイプ、FullyNamed
はプロトコルという感じで、メタタイプが変わってしまっている影響も考えられます。
メタタイプをプロトコルからタイプに変えると、FullyNamed
タイプにしてもsomething
は呼べないという動きになります。これはプロトコルが実装を持たないため、直接呼び出すことができないということです。
こうして、規定の実装を持たせたらどうなるかを試してみると、スタティックメソッドを持たせてみても、FullyNamed
型から直接スタティックメソッドを呼ぶことはやはりできません。
存在型の話に戻ると、FullyNamed
プロトコルを型として扱ったときに、プロトコルに準拠しているような型と思えますが、実装がない理由でスタティックメソッドを呼ぶことができないというところです。プロトコルには実装がないため、直接の呼び出しはできないという動作は、存在型という表現とどうつながるかを理解するのが難しいかもしれません。
実際にプロトコルの適合性を見てみると、ある変数value
がFullyNamed
プロトコル型で、そのインスタンスを持っている場合、そのバリューがFullyNamed
かチェックするとtrue
と出ます。これは予想通りですが、これをジェネリクスとして使おうとすると、FullyNamed
型はFullyNamed
プロトコルに準拠していないというエラーになります。
このように、FullyNamed
型はFullyNamed
プロトコルとして存在しますが、ジェネリクスには渡せないという動きを見せるのがプロトコル型の特徴です。
存在型という名前について、プロトコルに適合する形が存在する型
とされるのですが、ジェネリクスには渡せない点など、存在型と呼べるかどうかについて、自分自身はまだ完全に把握していないので、何とも言えないですね。 とりあえず、少なくともSwiftにはこういう事実があるということを意識に留めておく必要があるのかなと思います。
要所を整理すると、プロトコルは型として使えるけれども、その型として使ったプロトコルはそのプロトコルには準拠していない、という感じですね。
ちょっと話を戻しますけど、プロトコルが型として使える場面について一応整理しておきましょう。ほとんどの場所でプロトコルを型として使えます。具体的には、関数やメソッド、イニシャライザーの引数や戻り値として使えるということです。ここで、イニシャライザーの戻り値としては使えない場合について説明します。
例えば、プロトコル P
があって、これでイニシャライザーを規定していたとします。イニシャライザーというのは、戻り値がその型自身と捉えられます。例えば、型 V
のイニシャライザーの場合、戻り値は V
として捉えられます。したがって、関数としての型が引数を含まないデフォルトイニシャライザーで、戻り値が V
となります。
つまり、イニシャライザーの戻り値はその型自身なので、これがプロトコル P
に準拠したからといって、型 V
のイニシャライザーの戻り値が P
になるわけではありません。あくまで V
が返るのです。プロトコル P
のイニシャライザーを呼べるわけではないのです。
例えば、こんな感じです。関数やメソッドの中で P
を返すようなもので、自分自身がイニシャライザーが規定されている場合、大文字の Self
からコンパイル通りに書くと、表向きには戻り値として P
を返すように見えるかもしれません。しかし、実際にプロトコルを適用させると、インスタンス化した型 V
が返ることになります。
具体的には以下のようなコードです:
protocol P {
init()
}
class V: P {
required init() {}
func something() -> P {
return V() as P
}
}
let x = V()
let y = x.something() // y は P のインスタンス
このように、メソッドの戻り値が P
なだけであって、イニシャライザーが P
かどうかは別の話です。ちゃんとメソッドの戻り値が P
を返すのですね。
結論として、イニシャライザー自体は P
ではなく、大文字の Self
型を返すのが正しい認識ですね。
そのため、「Pを返すところを型として返していて、セルフイニットは V
だけど、V as P
が行われている」という解釈が妥当だと思います。実際のインスタンスは V
ですからね。 そうですね。なので、このスライドのイニシャライザーの戻り値の型としては、多分使えないと思ってていいかなと思います。あと、他に変数や変数プロパティの型として使えるようになっています。そして、辞書や配列などのコンテナが内包する要素としても使えるというふうに書いてあります。この辺りは問題なしですね。
アレイのプロトコル型を使いたい場合、プロトコル P
があったときに、アレイを要素 P
として規定することができるというお話です。これもお馴染みですよね。このように、ほとんどの場面でプロトコル型というものが使えます。今のところ見つかっている例外は、イニシャライザーの戻り値ぐらいですかね。
プロトコルの命名規則として、プロトコルは型であるため大文字で始めると「The Swift Programming Language」に書いてありました。「プロトコルは型であるため」というのはちょっと言い過ぎな感じがしますけど、気持ちは分かります。とりあえず型としても使えるから、他の型と同様に大文字で始める命名規則が用意されているということです。こんな感じですね。
ここまでオッケーです。プロトコル型についてイメージがついたかと思いますが、話しておきたいことがあって、「存在型の自己準拠」について若干どうでもいい話なんですけど、背景の色が白じゃなくなっているスライドをこれから自分独自で追加したスライドということにします。ディスプレイの性能によっては区別がつかないかもしれないですけど、一応そういう感じで作っていきますね。
ここからは、スライドは「The Swift Programming」になかったお話です。さっきのプレイグラウンドで見せたプロトコル型はそれ自身には準拠しないよというお話のおさらいですけど、さっき書いたとおりプロトコル型として使う分には問題ないけど、ジェネリクスとして使った途端プロトコル型は渡せなくなるというふうなお話です。
あともう一個おさらいですが、プロトコル型には実体がないため static
メンバー(今回プロパティですけど、さっきはメソッドで紹介しました)やイニシャライザーをプロトコル型として直接使うことはできないというお話です。これはプロトコルに実装がないという性格があって、実際には具体的な型と関連付かないと使えないのです。なので、これらはコンパイル段階では解決できないのでエラーになる、という感じです。
ここでちょっと面白いお話があるので紹介しておこうかなと思います。ジェネリクスでは使えるっていうところです。例えば、さっきのスライドにあったコードを使おうとすると、この FullyNamed
という型に対して organizationIdentifier
やイニシャライザーなどの static
系のメンバーは呼べないですけど、これをジェネリクスにすると呼べるようになります。具体的には T
が FullyNamed
で、その FullyNamed
を返すみたいな形にします。
protocol P {
// プロトコルの定義
}
func someFunction<T: P>(_ value: T) {
// 実装
}
こういった実装にすると、FullyNamed
プロトコル型でもいいんですけど、今回 P
にしましょう。そうすると、FullyNamed
として P
を使う、こういうふうに書いてあげるとこれでエラーがなくなります。ジェネリクスにすると呼べるようになります。これはコンパイルタイムには FullyNamed
型が固定されるわけではなく、何らかの型という形でコンパイルされて、実際には使われる型に応じて実行時に置き換わります。そうするとプロトコル型のときには実体がなかったけど、こういったジェネリック関数では型が必ず関連付けられているので、定的メソッドやイニシャライザーが呼び出せるようになります。これがジェネリクスの特徴です。
さっきのプロトコル型とジェネリクスの特徴的な動きの違いが、このポイントです。プロトコルはそれ自身をプロトコル型として使ったときに、それ自身には準拠しない、というお話をしましたが、ジェネリクスを使うとそれが可能になります。 これが特別にエラープロトコルと @objc
プロトコル、この2つについては自動的に準拠するという仕組みになっている点が面白いです。まずエラープロトコルを見ていきましょう。例えばエラープロトコルは Swift 標準ライブラリに入っているものです。それから派生したものは自動準拠されないのですが、エラープロトコルそのものについては準拠するのです。なので、例えば何らかのエラー型があって、これが Swift の標準のエラープロトコルに準拠しているとします。このときに、そのエラー型をプロトコル型として使ってみましょう。
protocol ErrorType: Error {}
仮にある関数がエラー型をパラメータとして受け取るような API になっていたときに、そのエラー型を渡すことができます。ジェネリックスを用いた例で説明すると、以下のようになります。例えば、ある型 T
がエラープロトコルに準拠していると指定した場合、そのエラーのインスタンスを問題なく渡せるということです。
func handleError<T: Error>(_ error: T) {
// エラー処理
}
このようにして、Swift 5 から搭載された自動準拠の機能を使うことができます。今までエラー型は汎用的に存在型として使われがちでしたが、ジェネリックスが使えないという制約がありました。それに困っていた人にとって、これからはジェネリックスとして具体的な型として扱えるようになるというポイントは把握しておいた方が良いですね。
次に @objc
プロトコルのお話です。@objc
を使うためには Foundation をインポートする必要があります。
import Foundation
例えば、あるプロトコル SomeProtocol
を定義し、このプロトコルに @objc
を付けていない場合を考えます。このとき、あるクラスが SomeProtocol
に準拠するとして、SomeProtocol
型としてオブジェクトをジェネリックスのパラメータに渡すとエラーになります。
protocol SomeProtocol {}
class SomeClass: SomeProtocol {}
func someFunction<T: SomeProtocol>(_ param: T) {
// 処理
}
このコードをコンパイルするとエラーになるはずです。しかし、SomeProtocol
に @objc
を付けると、自動的に準拠されてコンパイルが通るようになります。
@objc protocol SomeProtocol {}
ここがフレームワークなどで @objc
プロトコルを使う場面で役立ちます。プロトコル型をそのままジェネリックスのパラメータに渡していくことができるようになります。ただ、プロトコルは実装を持たないため、静的メンバーを呼ぶことはできないので注意が必要です。具体的な例として、プロトコルに静的メソッドを追加したときの動作を見てみましょう。
@objc protocol SomeProtocol {
static func someMethod() -> Int
}
class SomeClass: SomeProtocol {
static func someMethod() -> Int {
return 7
}
}
このとき、プロトコル型をジェネリックスに渡すことはできません。エラーとして、@objc
プロトコルに準拠するためには静的メンバーを持ってはいけないという制約が出てきます。以上のような特徴があることを覚えておくと良いでしょう。 ただ、先ほどのお話をちゃんと理解できていれば、「これがサムプロトコル型の T はジェネリックで表現しているときには型そのものが実際の具象型と関連付くので、この T に対しては静的メンバーを呼ぶことができる」ということがわかるかもしれません。自分は時々混乱して、なんでここで T が静的メンバーを使えるのか悩んでしまうのですが、こういう特徴があります。
また、ここで T ではなくサムプロトコル型だったとき、ここでサムプロトコル型として呼ばざるを得ないこともあります。さっきのお話のとおり、ちょっと面白いこととして、Type(of: value)
の something が呼べるんですよね。メタタイプを取ると静的メンバーが呼べるようになる、ということです。
バリューという変数にはランタイム時に具体的なインスタンスが渡されてきます。具体的なインスタンスは具体的な型と関連づいているので、ダイナミックキャストでキャストではなくタイプチェックでインスタンスから型を取ったとき、その型は具体的な型と関連づいているため、静的メンバーが呼べるようになります。
このような動きですから、メタタイプとしてサムプロトコルタイプ型があったときには、必ず具体的なオブジェクト型のメタタイプが入っているので、このメタタイプに対してはサムシングが呼べるようになる、という動きです。確かに、整理すると理解しやすくなりますね。
次に、先ほどの13行目で T.サムシング
が混乱する違和感がありましたよね。こちらについてですが、13行目の T: SamProtocol
をベアメタで書けるのではないかという意見が出ました。左側を SamProtocol
にせず、T だけにするということです。これは確かにわかりやすいかもしれません。サムプロトコル型が必須だから、T は具象型なんだなというイメージを持ちやすくなります。
また、この書き方が等価であることを検証するには、例えば Where 句を使って T: SamProtocol
とする場合と、そうでない場合の静的メソッドの呼び方を確認する方法があります。同じならば、エラーは発生しません。これで検証できますね。
以上で今日の勉強会は終了となります。資料は後ほどアップしておくので、わからなかったところがあれば再度確認していただけます。また次回は共変性と反変性についても触れながら、さらに深い話をしていく予定です。
それでは、お疲れ様でした。ありがとうございました。