概略
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で!(丸投げ) ↩