それはテストを書いている時に起きた・・・
プロダクトコードではprotocol extensionを使っている部分をモックで置き換えようとしたが、何故かモックのメソッドが呼ばれない!
確かにモック内でプロトコルのメソッドを定義しているはずなのに・・・
書いたコード(簡略化)
protocol SomeProtocol {
func hoge(string: String)
}
// プロダクトコードのときはこちらを通ってほしい
extension SomeProtocol {
func hoge(string: String = "") {
print("\(string)SomeProtocol")
}
}
// テスト用のモックだと思ってください
// テスト時はこちらを通したい
class SomeClass: SomeProtocol {
func hoge(string: String = "") {
print("\(string)SomeClass")
}
}
// 型としてはプロトコル
let some: SomeProtocol = SomeClass()
some.hoge(string: "hello ") // これらの
some.hoge() // 出力は・・・?
あるプロトコルにhoge(string:)
というメソッドが定義してあって、protocol extensionとSomeClass
と、どちらにも実装があるという状態です。
その時に、型としてはSomeProtocol
で実体はSomeClass
のプロパティからhoge()
を呼び出します。
期待する結果としては、どちらもSomeClass
のhoge()
が呼ばれてほしいです。
実際の結果は以下の通り。
some.hoge(string: "hello ") // => hello SomeClass
some.hoge() // => SomeProtocol
some.hoge(string: "hello ")
については期待通りSomeClass
のメソッドが呼ばれましたが、some.hoge()
はprotocol extensionのメソッドが呼ばれてしまいました。
何が起きているのか
protocol extensionの挙動については素晴らしい記事があるので、ぜひこちらを読んで下さい。
Swiftのプロトコルエクステンションの罠
今回のコードに関連する部分だけざっくりまとめると、
- プロトコル本体に宣言があるメソッドを呼び出す場合には動的ディスパッチ
- プロトコル本体に宣言が無い場合は静的ディスパッチ
になるということです。
もうおわかりかと思いますが、プロトコル本体で宣言があるのは、hoge(string:)
というラベルを持ったメソッドのみで、ラベルなしのhoge()
は宣言がありません。
そのためsome.hoge(string: "hello ")
は実体であるSomeClass
のメソッドが呼ばれ、some.hoge()
は型であるSomeProtocol
のメソッドが呼ばれました。
修正
プロトコル本体にhoge()
の宣言が無いことが問題だったので、追加してやれば解決です。
デフォルト引数はある意味がなくなってしまったので消しています。
protocol SomeProtocol {
func hoge() // 追加
func hoge(string: String)
}
extension SomeProtocol {
func hoge() { // 追加
self.hoge(string: "")
}
func hoge(string: String) {
print("\(string)SomeProtocol")
}
}
class SomeClass: SomeProtocol {
func hoge() { // 追加
self.hoge(string: "")
}
func hoge(string: String) {
print("\(string)SomeClass")
}
}
let some: SomeProtocol = SomeClass()
some.hoge(string: "hello ") // => hello SomeClass
some.hoge() // => SomeClass
これで期待通りの結果になりました!
おまけ
実は上のコードはもっとサボることができます。
protocol SomeProtocol {
//func hoge()
func hoge(string: String)
}
extension SomeProtocol {
func hoge() {
self.hoge(string: "")
}
func hoge(string: String) {
print("\(string)SomeProtocol")
}
}
class SomeClass: SomeProtocol {
// func hoge() {
// self.hoge(string: "")
// }
func hoge(string: String) {
print("\(string)SomeClass")
}
}
let some: SomeProtocol = SomeClass()
some.hoge(string: "hello ") // => hello SomeClass
some.hoge() // => SomeClass
コメントアウトした部分が上のコードとの差分です。
この場合のsome.hoge()
は一度SomeProtocol.hoge()
を経由してSomeClass.hoge(string:)
を呼び出します。
今回はhoge()
が中身の無いメソッドなのでこれでもできますが、普通はちゃんとプロトコルで宣言して実装するのが良いかと思います。
おわりに
protocol extensionはコードを共通化するのに非常に便利な機能なのですが、途中リンクを載せた記事の通り、若干不思議な挙動をするので、注意が必要です
デフォルト引数も便利なのですが、メソッドのラベルを書かないことでラベルありのメソッドと別ものと判断されてしまうのは罠でした
これで詰まった人とかいないんですかね
ちょっとニッチかもしれないけど、誰かしらの参考になれば幸いです