44
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swiftで継承とプロトコルとprotocol extensionが絡んだ時のメソッド呼び出し

Posted at

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を参照するようになっていないので、
サブクラスのメソッドは呼び出されません。

44
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?