概略
Swiftにおいて、protocol extensionで関数を実装したとしても、そのprotocolがその関数を必要とすると宣言しているか否かで、結果が変わることがあるよ。
(あとから気付いたのですが、別の方の記事の要約のようになってしまいました。詳しく知りたい方は当該記事をご覧ください。)
百聞は一見にしかず
この記事で言いたいことの全ては下記のコードが語ってくれます:
protocol P {
func f()
}
protocol Q {
// こっちは、からっぽ。
}
extension P {
func f() { print("P") }
}
extension Q {
func f() { print("Q") }
}
struct SP: P {
func f() { print("SP") }
}
struct SQ: Q {
func f() { print("SQ") }
}
SP().f() // "SP"と表示
SQ().f() // "SQ"と表示
func g<T>(_ p:T) where T:P { p.f() }
func g<T>(_ q:T) where T:Q { q.f() }
g(SP()) // "SP"と表示
g(SQ()) // "Q"と表示!!
Swiftに精通している方にとっては常識なのかもしれませんし、よく考えると当然の動作とは言えます。
素人による解説
SP().f() // "SP"と表示
SQ().f() // "SQ"と表示
これは期待を裏切らない動作ですね。SPもSQも関数f()を実装していることは分かっているので、それぞれの構造体で実装されているメソッドが呼ばれます。
それでは次を見てみましょう。
func g<T>(_ p:T) where T:P { p.f() }
func g<T>(_ q:T) where T:Q { q.f() }
g(SP()) // "SP"と表示
g(SQ()) // "Q"と表示!!
この動作の違いこそ、プロトコルが必要とする要件にfunc f()が宣言されているかどうかが生み出すものです。
プロトコルPにはfunc f()が必要と宣言されています。即ち、本来であればPに準拠する構造体(クラスや列挙型などもね)が実装の責任を負うことになります。ところで、関数gにとって引数pはPに準拠していることが判明しています。ということは、pはメソッドfunc f()を実装しているはずです。だからこそ、pそのものに実装されているfunc f()を安心して呼び出せます1。
一方、プロトコルQはfunc f()が必要とは宣言されていません。なので、関数gにとって、引数qがfunc f()を実装しているかどうかなんて分かりません。むしろ、実装していないと考えるのが自然(安全)でしょう。となると、qに対してfunc f()を呼び出そうとする危険な行為はせず、protocol extensionにあるfunc f()を呼ぶことになるでしょう。
-
SPからfunc f()を消すと、Pのprotocol extensionにあるfunc f()が呼び出されます。ここでの疑問は、“Pのprotocol extensionにfunc f()が実装されていたとしても、SPにfunc f()が実装されていなかったら、関数gでpに対してfunc f()を呼び出そうとした時に、なぜクラッシュしないのか?”ということです。これは予想ですが、コンパイラによって、SPに暗黙的にfunc f()を作るというようなこと(protocol extensionのfunc f()をコピーするまたはそれへ転送するような形)をしているのではないでしょうか。そうでなければ、func f()を動的に探索する際の選択肢にprotocol extensionのfunc f()を加えているということになりますが…。はたして?正解はGitHub/apple/swiftで!(丸投げ) ↩