このところずっと The Basic
の オプショナルバインディング
を眺めてきてますけれど、この話題でもう少しばかり続く様子です。これまでの中で オプショナルバインディング
をきっかけに周辺視野を良い具合に広げていけている感じがするので、そんな調子で今回もじっくり見ていけたらいいなと思っています。よろしくお願いしますね。
今回はゆめみ社外の人への一般公募はなかったようなので、基本的に社内メンバーのみでの開催になりそうです。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #161
00:00 開始 00:59 今回の話の発端 02:39 プロトコル型へのキャストと準拠性 03:24 型注釈ではなく型キャストとして働く 06:30 存在型は自分自身には準拠しない 12:35 プロトコル拡張とカスタマイズポイント 15:52 map はカスタマイズポイントではない 16:35 これらがジェネリクスの難しいところ 17:40 存在型が自身に準拠しないことについて 18:38 存在型が自己準拠する例外的な場面 22:37 将来は自身に準拠しているとみなされるかも? 23:06 イニシャライザーと自己準拠の関係 26:01 プロトコルを自身に準拠していると見做そうとする動きも 27:34 クロージング ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #161
さて、始めていきますけれど、今回はオプショナルバインディングの話です。前回はシャドウイングについて話しましたが、その続きになります。今回はシャドウイングではなく、オーバーロードの話に移っていきます。
前回確認した部分では、プロトコルの優先順位に関して、例えば、あるメソッドにInt
型を渡す場合、そのInt
型がプロトコルB
に準拠しているためB
として扱われ、その上でプロトコルA
も適用されるといった話でした。オーバーロードの優先順位については納得しやすい部分もありますが、属性を付けることで動きが変わるといったところが興味深いですね。
例えば、@available
などのアノテーションを使用すると、そのメソッドの優先順位が変わるといったことを話しました。また、アノテーションを付けた場合にどういった挙動になるのかを確認しました。
具体例を挙げると、次のようなコードがあります:
protocol A {}
protocol B: A {}
extension Int: B {}
func someMethod(value: A) {
print("A")
}
func someMethod(value: B) {
print("B")
}
let value: Int = 42
someMethod(value: value) // 結果は "B"
このコードでは、value
がInt
型であり、B
プロトコルに準拠しているため、B
として扱われます。しかし、これをA
として扱うにはどうしたら良いのかについて検討しました。
someMethod(value: value as A) // 結果は "A"
このようにキャストを使用することで、意図的にA
としてメソッドを呼び出すことができます。
さらに深掘りすると、型注釈やダウンキャスト、アップキャストについても議論しました。存在型(Existential Type)の概念も関わってきますが、この存在型はそれ自体がプロトコルには準拠していないものとして扱われる制約があるため、結果的にAny
を使った場合に意図しないメソッドが呼び出される可能性があります。
例えば、
let anyValue: Any = value
someMethod(value: anyValue) // 結果は "A"
このようにAny
を使うとA
としてのメソッドが呼び出されるという現象もあります。
この部分については、前提知識の違いにより納得できる人もいれば、そうでない人もいますが、プロトコルの存在型とその制約についての理解が大事になってきます。
これで今回のオプショナルバインディングとオーバーロードに関する補足説明は終わりです。また次回、新しいトピックについて学んでいきましょう。 なので、ここでは当然のように準拠していません。だからプロトコルAに準拠した方が渡ってきていないとみなされ、マッチしないんです。そうすると、ここでの回答が見えてきますね。
Something as A
はただ A
にもうちょっと直しておきましょうね。as A
は存在型 A
として値が使われ、それを渡すんですけれども、自分自身のプロトコルに準拠していないとみなされるので、当然のようにマッチしません。
7行目、もちろんそうですよね。プロトコルA自体はプロトコルBとは従来は関係していますけど、今回は関係していないので7行目にマッチすることはありません。そうすると、残ったもので A
には何でもいいようなので、いわゆる存在型の A
っていうのも受け入れてくれて、これで動くということでした。
自分もあの時はわからなかったんです。なんか理解はしているつもりなんですけど、前回のお話の中で「あれ、おかしいね」となってしまったから、その辺りが難しいんですよね。これから特にジェネリクスが一般的に活用されていくことになると思いますが、そのような場面になった時に、ちゃんと渡せるものだと思っていたのに渡せなかったというのは、なかなか疑問ですよね。
例えば思い当たるところとして、String
もプロトコルBに準拠させてみましょうか。そうすると、values
として値を入れておきます。例えば、T1
じゃなく A
や B
とかね。こういった時に、これが 1
とか 2
とか A
とか B
とか入るのかなと思ってやってみると、新しいキーワードなので、入りますよね。
この場合、values
として、print(values)
して print
テストをすると、何が出るでしょうか。これが A
にバンバン出るかもしれないですが、実際にコレクションとしては、存在型の B
として配列を揃えているものなので、そこから取り出した値はまた B
なわけです。この B
はジェネリクスに渡してもダメなんですよ。
これが怖いと感じるのは、実際にやってみないと理解しにくいという点です。しかし、今後 some
や any
のキーワードが使用されるようになったおかげで、こういったコードがどんどん出てくると思うんですよ。今まではジェネリクスに渡してもうまくいきませんでしたが、any
が使えるようになると書きやすくなりますね。
感覚として身につくまで、これが難しいんですよね。今回の話とは少し違いますが、ジェネリクスを使ったときとそうではないときの動きの微妙な違いが見えにくくなってしまいます。その意味でも、any
のキーワードが使えるようになると、今後ますますこういったコードが増えていくと思います。 わかったつもりになっていたけど、先輩の言う通りですね。自分も「なんか変だな」と感じることがあります。ただ、このあたりももしかすると型Aを入れる可能性が出てくることがあります。そうするとまたやばいかもしれません。
コメントを拾うと、プロトコルエクステンションの規定の実装の有無によって呼び出しが異なるとの意見があります。プロトコルエクステンションのデフォルト実装の有無によって呼び出しがどう変わるか、まさにその通りです。確かにプロトコルエクステンションでデフォルト実装があった場合に、プロトコルに準拠したオブジェクトがその実装を使うのか、自分自身で新たに実装するのかという問題があります。このあたりも混乱しやすいポイントですよね。
例えば、プロトコルPがあって、TypeAがPに準拠しているとします。このとき、TypeAのインスタンスを作成し、print(a.action)
とした場合、デフォルトではTypeAのactionが呼ばれることになります(ここでは仮にSとしておきます)。しかし、return S
と記述した場合、実行結果はSになります。
さらに、invokeAction
という関数を作成し、パラメータとしてプロトコルPに準拠したインスタンスを受け取り、そのインスタンスのactionを実行して返すとします。このとき、TypeSを渡した場合、カスタマイズポイントが導入されていない限り、SではなくPのデフォルト実装が呼ばれることになります。
「カスタマイズポイント」という言葉は、このような専門的なコンテキストで使われることがあります。プロトコル本体に定義されたAPIはカスタマイズポイントがあると言われることがあります。これがあるために実装が難しくなるんですね。
また、シーケンスやコレクションを操作する際に独自の実装をしたいと思うことがあるかもしれません。しかしカスタマイズポイントがない場合、独自の実装が動作してしまい罠にはまることがあります。このようにプロトコルは難しい側面があります。
類似の問題として、存在型と準拠性の問題もあります。存在型を利用する際の罠もありますが、これについては以前の勉強会で話されていますので、詳細はノーションを参照してください。
エラープロトコルについても触れておきます。例えば、struct MyError: Error
といった形でエラープロトコルに準拠させることができます。これにより、特定のエラー型として扱うことができます。
例えば、次のようなコードがあります:
struct MyError: Error {}
let error: Error = MyError()
この場合、error
はError
型ですが、実行時にはMyError
として扱われます。このような例外もありますので注意が必要です。
こうしたテーマについては非常に多くの議論があります。見るだけでも面白いので、是非他の資料も参照してみてください。 別に罠にかかったことはないですが、これは罠ですよね。5000年違う解釈をした特例があって、面白い例もあります。たとえば、あるオブジェクトを使います。これは何の変哲もないオブジェクトで、プロトコルBに準拠しています。このとき、let object = オブジェクト
とします。これで、オブジェクトを渡す場合と、オブジェクトをBとして、もしくはAにした場合、どちらも従来通りです。オブジェクトを直接渡すとプロトコルの判定がされますが、ジェネリクスとして渡さなければAny
となります。これをどうするか考えています。
例えば、import Foundation
してみて、let object = NSObject()
とします。これだけでできるでしょうか。NSObjectに準拠させるためには、さらにオブジェクトを追加する必要があります。NSオブジェクトに準拠したクラスであり、スタティックプロパティーしか持っていない場合、プロトコルに準拠しているものとみなされるというルールがあるようです。
どこで情報を見るべきか調べると出てくるでしょう。特例がいくつかあり、スタティックメンバーのプロトコル限定の場合もあるかもしれません。将来的には、存在型として成り立つようにしようという仕様変更が検討されている可能性もあります。
存在型がプロトコルに準拠しているとみなされた場合、特定の状況で困ることがあります。特にイニシャライザーの時に問題が生じます。イニシャライザーは自身のインスタンスを作ります。例えば、extension
で作られた型Xがあるとします。Xはデフォルトのイニシャライザーを持っているので、直接呼び出すことができます。しかし、型パラメーターXが具体的な型ではなく存在型として扱われると、イニシャライザーが隠蔽されてしまいます。このため、型そのものをXに準拠しているとして扱うと問題が生じます。
Swiftでは、イニシャライザーやスタティックメンバーを持たないエクステンシャルタイプをプロトコルに準拠しているものとして扱う特例があります。この特例を取り入れることで、言語組み込みの既存機能をユーザー定義パターンにも広げていく案が検討されています。しかし、それによって他の部分に影響が及び、動作が変わる場合もあるでしょう。
次にオプショナルバインディングの話題になる予定でしたが、今日はここで終わりにします。お疲れ様でした。ありがとうございました。