LoginSignup
13

More than 5 years have passed since last update.

【Swift】protocol extensionでデフォルト引数を使うときはメソッドの宣言に気をつけよう

Last updated at Posted at 2017-12-11

それはテストを書いている時に起きた・・・:fearful:
プロダクトコードではprotocol extensionを使っている部分をモックで置き換えようとしたが、何故かモックのメソッドが呼ばれない!:scream:
確かにモック内でプロトコルのメソッドを定義しているはずなのに・・・:confounded:

書いたコード(簡略化)

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()を呼び出します。
期待する結果としては、どちらもSomeClasshoge()が呼ばれてほしいです。
実際の結果は以下の通り。

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はコードを共通化するのに非常に便利な機能なのですが、途中リンクを載せた記事の通り、若干不思議な挙動をするので、注意が必要です:warning:
デフォルト引数も便利なのですが、メソッドのラベルを書かないことでラベルありのメソッドと別ものと判断されてしまうのは罠でした:bulb:
これで詰まった人とかいないんですかね:question:
ちょっとニッチかもしれないけど、誰かしらの参考になれば幸いです:ok_woman:

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
13