Swiftで継承とプロトコルとprotocol extensionが絡んだ時に呼び出されるメソッドがわかりづらい事があるのでまとめました。
バージョン
$ swift --version
Apple Swift version 4.2 (swiftlang-1000.0.36 clang-1000.10.44)
Target: x86_64-apple-darwin17.7.0
ソース
protocol AnimalProtocol {
func a()
func c()
func d()
}
extension AnimalProtocol {
func a() {
print("AnimalProtocol.a")
}
func b() {
print("AnimalProtocol.b")
}
func c() {
print("AnimalProtocol.c")
}
func d() {
defaultD(self)
}
}
func defaultD<X: AnimalProtocol>(_ obj: X) {
print("AnimalProtocol.d")
}
class Animal : AnimalProtocol {
func a() {
print("Animal.a")
}
func b() {
print("Animal.b")
}
func d() {
defaultD(self)
}
}
class Cat : Animal {
override func a() {
print("Cat.a")
}
override func b() {
print("Cat.b")
}
func c() {
print("Cat.c")
}
override func d() {
print("Cat.d")
}
}
func invokeA<X: AnimalProtocol>(_ x: X) {
x.a()
}
func invokeB<X: AnimalProtocol>(_ x: X) {
x.b()
}
func invokeC<X: AnimalProtocol>(_ x: X) {
x.c()
}
func invokeD<X: AnimalProtocol>(_ x: X) {
x.d()
}
let cat: Cat = Cat()
invokeA(cat) // => Cat.a
invokeB(cat) // => AnimalProtocol.b
invokeC(cat) // => AnimalProtocol.c
invokeD(cat) // => Cat.d
解説
上記のコードは、AnimalProtocol
というプロトコル、Animal
という親クラス、 Cat
というサブクラスにおいて、メソッドをどのように実装するかによって、挙動がどう異なるかを示したものです。
パターンはA, B, C, Dの4種類あります。
あるCat
型の式がある時、それに対するメソッド呼び出しは、Cat
で定義されたメソッドが呼ばれます。しかし、このコードのように<X: AnimalProtocol>
というジェネリックな型に対して呼び出した場合は、Cat
で定義されたものが呼ばれる場合と、そうでない場合があります。
パターンA
このパターンは以下のとおりです。
- プロトコル定義にメソッド定義が ある
- (共通)protocol extensionに実装が ある
- 親クラスに実装が ある
- サブクラスに
override func
実装が ある
この場合、サブクラスの実装が呼ばれます。
パターンB
このパターンは以下のとおりです。
- プロトコル定義にメソッド定義が ない
- (共通)protocol extensionに実装が ある
- 親クラスに実装が ある
- サブクラスに
override func
実装が ある
この場合、protocol extensionの実装が呼ばれます。
パターンC
このパターンは以下のとおりです。
- プロトコル定義にメソッド定義が ある
- (共通)protocol extensionに実装が ある
- 親クラスに実装が ない
- サブクラスに
func
実装が ある
この場合、protocol extensionの実装が呼ばれます。
パターンD
このパターンは、パターンAと同じ状態です。
ただし、親クラスの実装において、
protocol extensionと同様の処理を行うために、
そのデフォルト実装をトップレベル func
に切り出した上で、
親クラスの実装とprotocol extensionの実装の両方から、
その切り出したメソッドを呼び出すようにしています。
パターンCを書き換えてパターンAに変更したい場合に、
親クラスの実装でデフォルト実装を再利用したいができなくて不便、
という際の参考にしてください。
なぜこのような実行結果になるのか
Swiftにおいてはプロトコルのメソッドを実装したwitness tableと、
クラスのメソッドを実装したvtableというものがあります。
このようなプロトコルと継承が組み合わさったパターンでは、
インスタンスが親クラスでもサブクラスでも、
witness tableは親クラスのconformanceによって定義されたものが使われます。
そして、サブクラスのメソッドの呼び出しは、
witness tableを引いたその中で、vtableをさらに引く、
という2段ディスパッチによって行われます。
パターンAではこの2段ディスパッチによって、
サブクラスの実装が呼び出されます。
パターンBではプロトコルにメソッドの定義が無いため、
witness tableのレイアウトにそのメソッドのエントリが無いため、
protocol extensionの実装が呼び出されます。
パターンCではプロトコルにメソッドの定義があるため、
witness tableのレイアウトにそのメソッドのエントリはあるが、
親クラスにメソッドの定義が無いため、
そのエントリはprotocol extensionの実装がセットされており、
それが呼び出されます。
たとえサブクラスでメソッドを実装していても、
conformanceは親クラスで解決されており、
vtableを参照するようになっていないので、
サブクラスのメソッドは呼び出されません。