LoginSignup
61

More than 5 years have passed since last update.

posted at

updated at

SwiftにおけるMethod Dispatchについて

はじめに

2017/2/14のSwift愛好会 Vol16にて、発表させていただいた内容です。
概要だけを見たい方は、こちらのスライドを見てください。

実験環境

Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
Target: x86_64-apple-macosx10.9
Xcode 8.2

概要

Method Dispatchとは、あるメソッドを呼び出す場合に、実際に呼び出されるメソッドは、どのメソッドか、ということを選択する機構のことです。
以下のコード例を使って簡単に説明をします。

protocol AlcoholProtocol { }
extension AlcoholProtocol {
    func alcohol() -> String {
        return "🍶"
    }
}

class TwoBeers { }
extension TwoBeers: AlcoholProtocol {
    func alcohol() -> String {
        return "🍻"
    }
}

let beerServer: AlcoholProtocol = TwoBeers()
let myBeer = beerServer.alcohol()
print(myBeer)

上記のコード例では、AlcoholProtocolというプロトコルとTwoBeersというクラスを定義しています。
TwoBeersクラスは、extensionを使用して、AlcoholProtocolの実装を行なっています。
これらの型の定義後に実行しているコードにおいて、beerServerインスタンスのalcoholメソッドの呼び出しを行なっています。
alcoholメソッドは、AlcoholProtocolで定義されているメソッドです。
さらに、AlcoholProtocolのデフォルト実装も行われています。
また、AlcoholProtocolに準拠しているTwoBeersにおいても実装を行なっています。
そのため、let myBeer = beerServer.alcohol()の実行では、beerServer.alcohool()で、AlcoholProtocolで定義されているメソッドか、TwoBeersで定義されているメソッドのどちらかを呼び出すことになります。
ここで、どちらで定義されているメソッドを呼び出すかを決定する機構が、Method Dispatchになります。

Method Dispatchのタイミング

Method Dispatchは、呼び出すべきメソッドを決定することが目的になります。
呼び出すべきメソッドは、どのタイミングで決定されるのか、というと、大きく分けて以下のタイミングで決定されます。

  • コンパイル時 (Static Dispatch)
  • プログラム実行時 (Dynamic Dispatch)

コンパイル時に決定可能なメソッドとは、関数や構造体(struct)のメソッドなどです。
これらのメソッドは、同じシグネチャのメソッドは存在しないことが保証されるため、コンパイル時に、呼び出すべきメソッドが決定します。
(同じシグネチャのメソッドが複数存在する場合には、コンパイルエラーとなるためです)

実行時に呼び出すメソッドが決定されるメソッドとは、クラス(class)で定義されたメソッドです。
これらのメソッドは、同じシグネチャのメソッドが、基底クラスと派生クラスに、複数存在し、かつ、インスタンスを保持する変数の型と実際のインスタンスの型が異なる可能性があるため、コンパイル時に呼び出すメソッドを決定することができません。
つまり、以下のコード例のような場合です。

class Base {
    func method() { ... }
}
class Sub: Base {
    override func method() { ... }
}
let instance: Base = Sub()
instance.method()

このコード例では、以下のようになっています。

  • メソッドmethodが、基底クラスで定義されている
  • メソッドmethodは、派生クラスでオーバーライドされている
  • インスタンスinstanceの型は、基底クラスである
  • インスタンスinstanceは、派生クラスのイニシャライザを使用して生成されている

このコード例で、コンパイル時点の型情報を使用して呼び出すメソッドを決定した場合、instanceの型が基底クラスなので、基底クラスBaseで定義されたmethodが呼び出されてしまい、期待している動作と異なる動作になります。
したがって、実行時に、instanceの実際の値から、呼び出すメソッドを決定しなければ、期待している動作、つまり、派生クラスSubで定義されたmethodは呼び出すことができません。

Method Dispatchの種類

SwiftのMethod Dispatchは、以下のように分類できます。

名前 Dispatchのタイミング 説明
Direct コンパイル時 直接的にメソッドの呼び出しを行う
VTable 実行時 仮想テーブルという、クラスで定義されたメソッドのアドレスの配列を使用して、メソッドの呼び出しを行う
Message 実行時 クラス階層を走査し、呼び出すメソッドを選択し、メソッドの呼び出しを行う
Witness 実行時 Witnessテーブルという、プロトコルで定義されたメソッドのアドレスの配列を使用して、メソッドの呼び出しを行う

VTableは、C++などの言語で使用されている方式です。
Messageは、Objective-Cのメソッドの呼び出しの機構です。
Witnessは、Swiftに特有な機構かと思います。(私は、Swiftの実装を見ていて、初めて目にしました)
こちらは、プロトコルに準拠していれば、プロトコルで定義されているメソッドの情報が記載されます。
また、デフォルト実装のためのWitnessテーブルも作成されます。

Method Dispatchの方式の調べ方

最も確実なMethod Dispatchの調べ方は、デバッガを使うなりして、バイナリを解析する、という方法ですが、Swiftなどモダンなコンパイラを使用している場合は、他にも方法があります。
それは、コンパイラが生成する中間表現(Intermediate Representation, IR)を調べる方法です。
SwiftのIRとして、Swift Intermediate Language (SIL)を出力できます。
このSILを見ながら、Method Dispatchがどのように行われているかを調べます。

余談)
gccやllvmなどでは、ソース言語から、IRへ変換し、その後、ターゲットとなるCPU向けのバイナリを生成する方式を採用しています。
このような方式を採用することで、以下のようなメリットが得られます。

  • 高位レベルでの最適化と低位レベルでの最適化を適用することができる
  • IRからの変換部分を作成することで、異なるCPU向けのコンパイラを容易に作成できる
  • ソース言語からIRへの変換部分を作成するだけで、既存のIRからバイナリを生成する部分を利用できる

Static Dispatch

Direct Dispatch

Direct Dispatchは、型情報とメソッドのシグネチャから、呼び出すメソッドが決定可能な場合に、コンパイル時に呼び出すメソッドが決定される仕組みです。
つまり、継承が行われない値型(structenum)で定義されているメソッドの呼び出しに適用されます。

struct BaseStruct {
    func methodA() {
        print("Base Struct Method A")
    }
    func methodB() {
        print("Base Struct Method B")
    }
}

let s = BaseStruct()
s.methodA()
s.methodB()

上記のコードにて、メソッド呼び出し部分のSILは、以下のようになります。

  // function_ref BaseStruct.methodA() -> ()
  %10 = function_ref @_TFV6Struct10BaseStruct7methodAfT_T_ : $@convention(method) (BaseStruct) -> (), loc "Struct.swift":11:3, scope 1 // user: %12
  %11 = load %5 : $*BaseStruct, loc "Struct.swift":11:1, scope 1 // user: %12
  %12 = apply %10(%11) : $@convention(method) (BaseStruct) -> (), loc "Struct.swift":11:11, scope 1
  // function_ref BaseStruct.methodB() -> ()
  %13 = function_ref @_TFV6Struct10BaseStruct7methodBfT_T_ : $@convention(method) (BaseStruct) -> (), loc "Struct.swift":12:3, scope 1 // user: %15
  %14 = load %5 : $*BaseStruct, loc "Struct.swift":12:1, scope 1 // user: %15
  %15 = apply %13(%14) : $@convention(method) (BaseStruct) -> (), loc "Struct.swift":12:11, scope 1

上記のコードでは、メソッドのアドレスが、それぞれ、%10%13に格納されます。
%5には、構造体BaseStructが存在するアドレスが格納されています。
最後に、メソッドの呼び出しを行なっているapply命令により、BaseStructのメソッドが呼び出されている、ということが分かります。

Dynamic Dispatch

VTable (仮想テーブル)

仮想テーブルには、クラスで定義されたメソッドに対して、定義されているメソッドとメソッドの呼び出し先の情報が記録されています。
メソッドの呼び出し時に、仮想テーブルから呼び出すメソッドを探索し、そのメソッドの呼び出し先の情報を得て、呼び出すべきメソッドを決定します。
この方式は、参照型(class)で定義されているメソッドに対して行われます。

class BaseClass {
    func methodA() {
        print("Base Class Method A")
    }
    func methodB() {
        print("Base Class Method B")
    }
}

let c = BaseClass()
c.methodA()
c.methodB()

上記のコード例では、クラスBaseClassの仮想テーブルは以下のようになります。

sil_vtable BaseClass {
  #BaseClass.methodA!1: _TFC5Class9BaseClass7methodAfT_T_   // BaseClass.methodA() -> ()
  #BaseClass.methodB!1: _TFC5Class9BaseClass7methodBfT_T_   // BaseClass.methodB() -> ()
  #BaseClass.deinit!deallocator: _TFC5Class9BaseClassD  // BaseClass.__deallocating_deinit
  #BaseClass.init!initializer.1: _TFC5Class9BaseClasscfT_S0_    // BaseClass.init() -> BaseClass
}

仮想テーブルには、ソースコードで定義されているmethodAmethodB、暗黙的に定義されるイニシャライザとでイニシャライザが存在しています。
仮想テーブルの各エントリを見ると、それぞれのメソッドは、BaseClassで定義されているメソッドを指していることが分かります。
_TFC5Class9BaseClassのプリフィックスが付いていることから、BaseClassのメソッドが呼ばれることが分かります。

メソッド呼び出し部分のSILは、以下のようになります。

  %10 = load %5 : $*BaseClass, loc "Class.swift":11:1, scope 1 // users: %12, %11
  %11 = class_method %10 : $BaseClass, #BaseClass.methodA!1 : (BaseClass) -> () -> () , $@convention(method) (@guaranteed BaseClass) -> (), loc "Class.swift":11:3, scope 1 // user: %12
  %12 = apply %11(%10) : $@convention(method) (@guaranteed BaseClass) -> (), loc "Class.swift":11:11, scope 1
  %13 = load %5 : $*BaseClass, loc "Class.swift":12:1, scope 1 // users: %15, %14
  %14 = class_method %13 : $BaseClass, #BaseClass.methodB!1 : (BaseClass) -> () -> () , $@convention(method) (@guaranteed BaseClass) -> (), loc "Class.swift":12:3, scope 1 // user: %15
  %15 = apply %14(%13) : $@convention(method) (@guaranteed BaseClass) -> (), loc "Class.swift":12:11, scope 1

SILのclass_method命令により、クラスのインスタンスから動的に解決される、ということが分かります。
class_methodを使用して、メソッドの呼び出しが行われていることから、sil_vtableの情報を使用して、動的に呼び出すメソッドが決定される、ということが分かります。

次に、派生クラスを定義した場合にどうなるか、を見てみます。

class BaseClass {
    func methodA() {
        print("Base Class Method A")
    }
    func methodB() {
        print("Base Class Method B")
    }
}

class SubClass: BaseClass {
    override func methodB() {
        print("Sub Class Method B")
    }

    func methodC() {
        print("Sub Class Method C")
    }
}

let c: BaseClass = SubClass()
c.methodA()
c.methodB()

let s: SubClass = SubClass()
s.methodA()
s.methodB()
s.methodC()

BaseClassの仮想テーブルは、以下のようになります。

sil_vtable BaseClass {
  #BaseClass.methodA!1: _TFC8SubClass9BaseClass7methodAfT_T_    // BaseClass.methodA() -> ()
  #BaseClass.methodB!1: _TFC8SubClass9BaseClass7methodBfT_T_    // BaseClass.methodB() -> ()
  #BaseClass.deinit!deallocator: _TFC8SubClass9BaseClassD   // BaseClass.__deallocating_deinit
  #BaseClass.init!initializer.1: _TFC8SubClass9BaseClasscfT_S0_ // BaseClass.init() -> BaseClass
}

SubClassの仮想テーブルは、以下のようになります。

sil_vtable SubClass {
  #BaseClass.methodA!1: _TFC8SubClass9BaseClass7methodAfT_T_    // BaseClass.methodA() -> ()
  #BaseClass.methodB!1: _TFC8SubClass8SubClass7methodBfT_T_ // SubClass.methodB() -> ()
  #BaseClass.init!initializer.1: _TFC8SubClass8SubClasscfT_S0_  // SubClass.init() -> SubClass
  #SubClass.methodC!1: _TFC8SubClass8SubClass7methodCfT_T_  // SubClass.methodC() -> ()
  #SubClass.deinit!deallocator: _TFC8SubClass8SubClassD // SubClass.__deallocating_deinit
}

SubClassの仮想テーブルからは、以下のことが分かります。

  • methodAmethodBは、BaseClassで定義されている
    • #BaseClass.となっている点から
  • methodCは、SubClassで定義されている
    • #SubClass.となっている点から
  • methodBは、SubClassでオーバーライドされている
    • _TFC8SubClass8SubClass7というプリフィックスが付いていることから
c.methodA()
c.methodB()

上記のコードに対応するSILは、以下のようになります。

  %11 = load %5 : $*BaseClass, loc "SubClass.swift":21:1, scope 1 // users: %13, %12
  %12 = class_method %11 : $BaseClass, #BaseClass.methodA!1 : (BaseClass) -> () -> () , $@convention(method) (@guaranteed BaseClass) -> (), loc "SubClass.swift":21:3, scope 1 // user: %13
  %13 = apply %12(%11) : $@convention(method) (@guaranteed BaseClass) -> (), loc "SubClass.swift":21:11, scope 1
  %14 = load %5 : $*BaseClass, loc "SubClass.swift":22:1, scope 1 // users: %16, %15
  %15 = class_method %14 : $BaseClass, #BaseClass.methodB!1 : (BaseClass) -> () -> () , $@convention(method) (@guaranteed BaseClass) -> (), loc "SubClass.swift":22:3, scope 1 // user: %16
  %16 = apply %15(%14) : $@convention(method) (@guaranteed BaseClass) -> (), loc "SubClass.swift":22:11, scope 1

SILの命令を見る限りでは、BaseClassのメソッドが呼ばれているように見えます。
したがって、let c: BaseClass = SubClass()を実行している部分を見てみます。

  alloc_global @_Tv8SubClass1cCS_9BaseClass, loc "SubClass.swift":20:5, scope 1 // id: %4
  %5 = global_addr @_Tv8SubClass1cCS_9BaseClass : $*BaseClass, loc "SubClass.swift":20:5, scope 1 // users: %14, %11, %10
  // function_ref SubClass.__allocating_init() -> SubClass
  %6 = function_ref @_TFC8SubClass8SubClassCfT_S0_ : $@convention(method) (@thick SubClass.Type) -> @owned SubClass, loc "SubClass.swift":20:20, scope 1 // user: %8
  %7 = metatype $@thick SubClass.Type, loc "SubClass.swift":20:20, scope 1 // user: %8
  %8 = apply %6(%7) : $@convention(method) (@thick SubClass.Type) -> @owned SubClass, loc "SubClass.swift":20:29, scope 1 // user: %9
  %9 = upcast %8 : $SubClass to $BaseClass, loc "SubClass.swift":20:20, scope 1 // user: %10
  store %9 to %5 : $*BaseClass, loc "SubClass.swift":20:20, scope 1 // id: %10

インスタンスの実体の型情報が%5へ格納されます。
この型情報を使用して、メソッドの呼び出しが行われます。
したがって、インスタンス生成時の型情報を使用して、#BaseClass.methodA!1#BaseClass.methodB!1の呼び出しが行われます。
この時、SubClassの仮想テーブルを参照します。(型情報より、SubClassの仮想テーブルの参照が行われます)
この仮想テーブルより、#BaseClass.methodA!1BaseClassのメソッドを呼び出す、ということが分かります。
また、#BaseClass.methodB!1は、SubClassのメソッドを呼び出す、ということが分かります。

Message

メッセージを使用したDispatchでは、クラス階層を走査し、当該クラスに呼び出すメソッドがあれば、そのメソッドの呼び出しを行います。
メッセージを使用したDispatchでは、Objective-Cランタイムを使用するため、Foundationをインポートする必要があります。

import Foundation

class BaseClass {
    dynamic func methodA() {
        print("Base Class Method A")
    }
    dynamic func methodB() {
        print("Base Class Method B")
    }
}

class SubClass: BaseClass {
    override func methodB() {
        print("Sub Class Method B")
    }

    func methodC() {
        print("Sub Class Method C")
    }
}

let c: BaseClass = SubClass()
c.methodA()
c.methodB()

let s: SubClass = SubClass()
s.methodA()
s.methodB()
s.methodC()

上記のコード例のc.methodA()のSILは以下のようになります。

  %11 = load %5 : $*BaseClass, loc "Class_Message.swift":23:1, scope 1 // users: %13, %12
  %12 = class_method [volatile] %11 : $BaseClass, #BaseClass.methodA!1.foreign : (BaseClass) -> () -> () , $@convention(objc_method) (BaseClass) -> (), loc "Class_Message.swift":23:3, scope 1 // user: %13
  %13 = apply %12(%11) : $@convention(objc_method) (BaseClass) -> (), loc "Class_Message.swift":23:11, scope 1

@convention(objc_method)を使用していることから、Objective-Cのメッセージングの機構を使用して、メソッドの呼び出しが行われていることが分かります。

Witness

Witnessテーブルは、プロトコルに準拠している場合に、どの型がどのプロトコルに準拠しているのか、という情報が記録されています。
メソッド呼び出し時には、Witnessテーブルを使用して、呼び出すメソッドを決定します。

protocol BaseProtocol {
    func protocolMethod()
}

class BaseClass {
    func methodA() {
        print("Base Class Method A")
    }
    func methodB() {
        print("Base Class Method B")
    }
}

extension BaseClass: BaseProtocol {
    func protocolMethod() {
        print("Protocol Method")
    }
}

let s: BaseProtocol = BaseClass()
s.protocolMethod()

上記のコード例から、BaseClassのWitnessテーブルは、以下のようになります。

sil_witness_table hidden BaseClass: BaseProtocol module main {
  method #BaseProtocol.protocolMethod!1: @_TTWC4main9BaseClassS_12BaseProtocolS_FS1_14protocolMethodfT_T_   // protocol witness for BaseProtocol.protocolMethod() -> () in conformance BaseClass
}

sil_default_witness_table hidden BaseProtocol {
  no_default
}

これらのWitnessテーブルから以下のことが分かります。

  • BaseClassは、BaseProtocolに準拠している
  • BaseProtocolには、デフォルト実装は存在しない

s.protocolMethod()に対応するSILは、以下のようになります。

  %11 = open_existential_addr %5 : $*BaseProtocol to $*@opened("978007AE-F39C-11E6-84C7-60F81DA97198") BaseProtocol, loc "Class+Protocol.swift":21:3, scope 1 // users: %13, %13, %12
  %12 = witness_method $@opened("978007AE-F39C-11E6-84C7-60F81DA97198") BaseProtocol, #BaseProtocol.protocolMethod!1, %11 : $*@opened("978007AE-F39C-11E6-84C7-60F81DA97198") BaseProtocol : $@convention(witness_method) <τ_0_0 where τ_0_0 : BaseProtocol> (@in_guaranteed τ_0_0) -> (), loc "Class+Protocol.swift":21:3, scope 1 // user: %13
  %13 = apply %12<@opened("978007AE-F39C-11E6-84C7-60F81DA97198") BaseProtocol>(%11) : $@convention(witness_method) <τ_0_0 where τ_0_0 : BaseProtocol> (@in_guaranteed τ_0_0) -> (), loc "Class+Protocol.swift":21:18, scope 1

最終行のapply命令にて、@convention(witness_method)を使用していることから、Witnessテーブルを使用し、呼び出すメソッドを決定していることが分かります。
また、ここで呼び出すメソッドがBaseProtocolで定義されている、ということも分かります。

final

classclassで定義されるメソッドには、修飾子としてfinalをつけることができます。
finalをつけることで、classの継承を不可能にする、メソッドを派生クラスでオーバーライドできないようにする、ということが可能になります。
つまり、基底クラスにおいて、finalを付けて定義されたメソッドは、どの派生クラスから見ても、必ず基底クラスのメソッドが呼ばれる、ということが保証されます。
そのため、このようなメソッドであれば、コンパイル時に呼び出すメソッドを決定することが可能となります。

class BaseClass {
    final func methodA() {
        print("Base Class Method A")
    }
    func methodB() {
        print("Base Class Method B")
    }
}

class SubClass: BaseClass {
    override func methodB() {
        print("Sub Class Method B")
    }
}

let c: SubClass = SubClass()
c.methodA()
c.methodB()

上記のコード例において、methodAは、基底クラスでfinalとして定義しています。
methodBは、finalなしの通常のメソッドとして定義しています。
このコードから生成したSILから、仮想テーブルを抜き出すと以下のようになります。

sil_vtable BaseClass {
  #BaseClass.methodB!1: _TFC10FinalClass9BaseClass7methodBfT_T_ // BaseClass.methodB() -> ()
  #BaseClass.deinit!deallocator: _TFC10FinalClass9BaseClassD    // BaseClass.__deallocating_deinit
  #BaseClass.init!initializer.1: _TFC10FinalClass9BaseClasscfT_S0_  // BaseClass.init() -> BaseClass
}

sil_vtable SubClass {
  #BaseClass.methodB!1: _TFC10FinalClass8SubClass7methodBfT_T_  // SubClass.methodB() -> ()
  #BaseClass.init!initializer.1: _TFC10FinalClass8SubClasscfT_S0_   // SubClass.init() -> SubClass
  #SubClass.deinit!deallocator: _TFC10FinalClass8SubClassD  // SubClass.__deallocating_deinit
}

仮想テーブルを見ると、methodAは、仮想テーブルに定義されていないことが分かります。
次に、methodAの呼び出しを行なっている箇所を示します。

  // function_ref BaseClass.methodA() -> ()
  %10 = function_ref @_TFC10FinalClass9BaseClass7methodAfT_T_ : $@convention(method) (@guaranteed BaseClass) -> (), loc "FinalClass.swift":17:3, scope 1 // user: %14
  %11 = load %5 : $*SubClass, loc "FinalClass.swift":17:1, scope 1 // users: %13, %12
  strong_retain %11 : $SubClass, loc "FinalClass.swift":17:1, scope 1 // id: %12
  %13 = upcast %11 : $SubClass to $BaseClass, loc "FinalClass.swift":17:1, scope 1 // users: %15, %14
  %14 = apply %10(%13) : $@convention(method) (@guaranteed BaseClass) -> (), loc "FinalClass.swift":17:11, scope 1
  strong_release %13 : $BaseClass, loc "FinalClass.swift":17:11, scope 1 // id: %15

まず、function_ref命令を使用し、methodAの参照を取得しています。
次に、SubClassの読み込みと、BaseClassへのキャストが行われています。
最後に、apply命令で、BaseClassmethodAが呼ばれています。
function_refを使用していることから、methodAは、参照型として扱われているのではなく、値型のオブジェクトとして扱われていることが分かります。
つまり、基底クラスにおいて、finalで定義されたメソッドには、Static Dispatchが適用される(コンパイル時に呼び出すメソッドが決定される)ということが分かります。

Extension

Swiftでは、extensionを使用することにより、既存のstruct, enum, classなどに、新しい機能を付け加えることができます。
ここで、extensionで定義されたメソッドは、どのように呼び出されるかを見てみます。

class BaseClass {
    func methodA() {
        print("Base Class Method A")
    }
    func methodB() {
        print("Base Class Method B")
    }
}

extension BaseClass {
    func methodC() {
        print("Base Class / Extension Method C")
    }
}

let c = BaseClass()
c.methodA()
c.methodB()
c.methodC()

BaseClassextensionとして、methodCを定義しています。
BaseClassの仮想テーブルを以下に示します。

sil_vtable BaseClass {
  #BaseClass.methodA!1: _TFC4main9BaseClass7methodAfT_T_    // BaseClass.methodA() -> ()
  #BaseClass.methodB!1: _TFC4main9BaseClass7methodBfT_T_    // BaseClass.methodB() -> ()
  #BaseClass.deinit!deallocator: _TFC4main9BaseClassD   // BaseClass.__deallocating_deinit
  #BaseClass.init!initializer.1: _TFC4main9BaseClasscfT_S0_ // BaseClass.init() -> BaseClass
}

extensionで定義したmethodCは、仮想テーブルに定義されていないことが分かります。
次に、methodCの呼び出しを行うSILを示します。

  // function_ref BaseClass.methodC() -> ()
  %16 = function_ref @_TFC4main9BaseClass7methodCfT_T_ : $@convention(method) (@guaranteed BaseClass) -> (), loc "Class+Extension.swift":19:3, scope 1 // user: %18
  %17 = load %5 : $*BaseClass, loc "Class+Extension.swift":19:1, scope 1 // user: %18
  %18 = apply %16(%17) : $@convention(method) (@guaranteed BaseClass) -> (), loc "Class+Extension.swift":19:11, scope 1

methodCは、function_ref命令を使用して、呼び出されていることが分かります。
つまり、extensionで定義されたメソッドには、Static Dispatchが適用されてます。

概要で示したコードの解説

以下に、概要で示したコードを再掲します。

protocol AlcoholProtocol { }
extension AlcoholProtocol {
    func alcohol() -> String {
        return "🍶"
    }
}

class TwoBeers { }
extension TwoBeers: AlcoholProtocol {
    func alcohol() -> String {
        return "🍻"
    }
}

let beerServer: AlcoholProtocol = TwoBeers()
let myBeer = beerServer.alcohol()
print(myBeer)

このとき、Witnessテーブルは、以下のようになります。

sil_witness_table hidden TwoBeers: AlcoholProtocol module ExampleProtocol {
}

sil_default_witness_table hidden AlcoholProtocol {
}

Witnessテーブルにメソッドが存在しないことから、AlcoholProtocolに準拠している型が実装すべきメソッドはないことが分かります。

次に、beerServer.alcohol()を実行している箇所のSILを示します。

  alloc_global @_Tv15ExampleProtocol6myBeerSS, loc "ExampleProtocol.swift":16:5, scope 1 // id: %11
  %12 = global_addr @_Tv15ExampleProtocol6myBeerSS : $*String, loc "ExampleProtocol.swift":16:5, scope 1 // users: %25, %16
  %13 = open_existential_addr %5 : $*AlcoholProtocol to $*@opened("DF620DBE-F45B-11E6-BBA0-60F81DA97198") AlcoholProtocol, loc "ExampleProtocol.swift":16:25, scope 1 // users: %15, %15
  // function_ref AlcoholProtocol.alcohol() -> String
  %14 = function_ref @_TFE15ExampleProtocolPS_15AlcoholProtocol7alcoholfT_SS : $@convention(method) <τ_0_0 where τ_0_0 : AlcoholProtocol> (@in_guaranteed τ_0_0) -> @owned String, loc "ExampleProtocol.swift":16:25, scope 1 // user: %15
  %15 = apply %14<@opened("DF620DBE-F45B-11E6-BBA0-60F81DA97198") AlcoholProtocol>(%13) : $@convention(method) <τ_0_0 where τ_0_0 : AlcoholProtocol> (@in_guaranteed τ_0_0) -> @owned String, loc "ExampleProtocol.swift":16:33, scope 1 // user: %16
  store %15 to %12 : $*String, loc "ExampleProtocol.swift":16:33, scope 1 // id: %16

初めの2行と最後の行は、変数myBeerの割り当てとalcoholメソッドの戻り値を書き込んでいる処理になります。
つまり、メソッド呼び出しにあたる部分は、以下の箇所になります。

  %13 = open_existential_addr %5 : $*AlcoholProtocol to $*@opened("DF620DBE-F45B-11E6-BBA0-60F81DA97198") AlcoholProtocol, loc "ExampleProtocol.swift":16:25, scope 1 // users: %15, %15
  // function_ref AlcoholProtocol.alcohol() -> String
  %14 = function_ref @_TFE15ExampleProtocolPS_15AlcoholProtocol7alcoholfT_SS : $@convention(method) <τ_0_0 where τ_0_0 : AlcoholProtocol> (@in_guaranteed τ_0_0) -> @owned String, loc "ExampleProtocol.swift":16:25, scope 1 // user: %15
  %15 = apply %14<@opened("DF620DBE-F45B-11E6-BBA0-60F81DA97198") AlcoholProtocol>(%13) : $@convention(method) <τ_0_0 where τ_0_0 : AlcoholProtocol> (@in_guaranteed τ_0_0) -> @owned String, loc "ExampleProtocol.swift":16:33, scope 1 // user: %16

function_ref命令が、使われていることからAlcoholProtocol.alcoholメソッドを使用していることが分かります。
つまり、Static dispatchにより、呼び出すメソッドが決定されている、ということが分かります。
結果として、上記のコードを実行すると 🍶 が出力されます。

最後に

今回は、SwiftにおけるMethod Dispatchを実例を交えて、解説を行いました。
一般的な使い方をしている限りは、あまり意識をしなくても問題となるケースは少ないかと思います。
extensionを多用している場合には、Dispatchの方式が、拡張している型のDispatch方法と異なるため、注意が必要になります。

補足

「こういうのって、どうやって調べているんですか?」と質問を複数の方から頂いたので、自分の調べ方を簡単にまとめると、以下のような感じです。

基本的には、上記から辿れる場所に情報があるので、あとは、実際にコードを書いて、どういうものが出来上がるのか、を検証する、という方法です。

その他、頂いていた質問は、別途、記事にまとめる予定です。

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
What you can do with signing up
61