はじめに
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は、型情報とメソッドのシグネチャから、呼び出すメソッドが決定可能な場合に、コンパイル時に呼び出すメソッドが決定される仕組みです。
つまり、継承が行われない値型(struct
やenum
)で定義されているメソッドの呼び出しに適用されます。
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
}
仮想テーブルには、ソースコードで定義されているmethodA
とmethodB
、暗黙的に定義されるイニシャライザとでイニシャライザが存在しています。
仮想テーブルの各エントリを見ると、それぞれのメソッドは、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
の仮想テーブルからは、以下のことが分かります。
-
methodA
、methodB
は、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!1
はBaseClass
のメソッドを呼び出す、ということが分かります。
また、#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
class
やclass
で定義されるメソッドには、修飾子として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
命令で、BaseClass
のmethodA
が呼ばれています。
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()
BaseClass
のextension
として、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方法と異なるため、注意が必要になります。
補足
「こういうのって、どうやって調べているんですか?」と質問を複数の方から頂いたので、自分の調べ方を簡単にまとめると、以下のような感じです。
基本的には、上記から辿れる場所に情報があるので、あとは、実際にコードを書いて、どういうものが出来上がるのか、を検証する、という方法です。
その他、頂いていた質問は、別途、記事にまとめる予定です。