🧐

プロトコルにデフォルト引数を利用したい場合のエクステンションの書き方の整理

2022/05/06に公開

iOSアプリで個人開発をしながら、Swiftや設計パターンの学習をしています。

今回はPresenterの設計を学ぶのに当たってprotocolを積極的に利用していたのですが、その過程で引数にデフォルトで値を与えたい場面が出てきました。プロトコルの記載の仕方を十分理解していなかったので、通常作るメソッドに与える引数と同じようにやったところ、次のようなエラーが表示されてしまいました。少し理解し直すまでに時間がかかってしまったため、この機に整理しました。

//① 
protocol TestProtocol {
        //Default argument not permitted in a protocol method
    func test(a: Int, b: Int = 15)  -> Int
}

エラー文に書かれているように、プロトコルでは直接引数の値をデフォルトで与えることはできません。ただ、与える方法がないわけではなく、結果的にデフォルト引数のような役割での値を渡すことは可能です。そのために必要であるのがextensionでのデフォルト実装(既定実装)です。

書き方

冒頭で確認したようにプロトコルには直接デフォルト引数を利用できませんが、エクステンションでの実装を通してプロトコル内のメソッド等に値を渡すことで、実質的にはデフォルト引数として利用することができます。

//① 通常のプロトコル
protocol TestProtocol {
    func testA(a: Int, b: Int) -> (Int,Int)
}

extension TestProtocol {
    //② デフォルト実装(規定実装)
    func testB(a: Int, b: Int = 15) -> (Int,Int) {
	//①引数に②引数の値を代入するという処理
        testA(a: a, b: b) 
    }
}

注意する必要があるのは、①と②は一応異なるメソッドであることです。エクステンションはプロトコルに記載されているメソッドに引数をデフォルトで与えるようなものではないということを示すために、あえて①をtestA、②をtestBとしていますが、実際に利用する上でAやBを区別すること必要はありません。

testBメソッドの中でtestAを呼び、testBにデフォルトで設定されている引数の値をtestAに渡すことで、本来デフォルト引数を与えられないはずのプロトコルのメソッドであるtestAが、あたかもあったかのように利用できるということわけですね。

自分は別個のメソッドだと認識できておらず、エクステンションを使えば通常のメソッドの引数名にデフォルトの値を追記で書けるのだと思い込んでしまったため、誤解が解けるまでに時間がかかってしまいました。

使い方

デフォルト実装は既に実装されているものとして扱われるので、プロトコルを適合したクラスや構造体内部にそのメソッドなどを書く必要がありません。というか、書くとinvalidな定義として弾かれると思います。実際のコードで使い方を確認してみます。ここではAとBの区別をなくしています。

//①通常のプロトコル
protocol TestProtocol {
    func test(a: Int, b: Int) -> (Int,Int)
}

extension TestProtocol {
        //②デフォルト実装
    func test(a: Int, b: Int = 15)  -> (Int,Int) {
        test(a: a, b: b)
    }
}

//①プロトコルに適合させた構造体
struct TestStruct: TestProtocol {
    //①プロトコルのメソッドを準拠し、処理を記述
    //②デフォルト実装はプロトコルに適合させた時点で準拠されている
    func test(a: Int, b: Int) -> (Int,Int) {
        return (a,b)
    }
}

let testStruct = TestStruct()

//① 呼んでいるのはプロトコルのメソッド
let a = testStruct.test(a: 10, b: 20) 

//② 呼んでいるのはデフォルト実装のメソッド
//ここでは b = 15がデフォルトで与えられている
let b = testStruct.test(a: 10) 

見るとわかりますが、プロトコルに適合した構造体に、デフォルト実装のメソッドを記述する必要はありません。処理が記述されているので、プロトコルに適合した時点で呼び出すことができます。

デフォルトらしさを考えれば命名は同一

デフォルトという本来の言葉の意味を考えたら、エクステンションで引数を与えたいメソッドの命名は、プロトコルに記載されているメソッド名と同じである方が自然なのだと思います。結局、処理の中身はプロトコルのメソッドを呼び出しているだけであるので。

使い方の例で考えてみます。

//① 呼んでいるのはプロトコルのメソッド
let a = testStruct.test(a: 10, b: 20) 

//② 呼んでいるのはデフォルト実装のメソッド
//ここでは b = 15がデフォルトで与えられている
let b = testStruct.test(a: 10) 

区別がつきやすいように冒頭ではtestA、testBとメソッド名を分けましたが、結局のところデフォルトの引数を渡すための中継地点でしかなく、本来プロトコルのメソッドの処理を走らせたいのだと考えると、命名を分けないのが自然なのだと思います。引数を確認すれば、デフォルトの値が与えられているかがわかります。

また、下記で紹介しますが、厳密に言えば名称が同じメソッドかどうかでXcode内部での処理の仕方が異なっている可能性があります。正直、この辺りはわかっていません。

一応の注意

プロトコルにデフォルトの値を利用することについて書かれている記事に、こういう場合にクラッシュするよというのが注意書きであったので、紹介します。

https://medium.com/@georgetsifrikas/swift-protocols-with-default-values-b7278d3eef22

Important note Because protocol BazProtocol has already a default implementation from the extension if you use it in an object and forgot to implement function foo compiler won’t complaint, but extension function foo will call itself recursively until your code crashes with BAD_ACCESS.

上記読む限りはクラッシュが生じるのにコード上はコンパイラはcomplaintされない、つまり通ってしまうだろうということが書かれていて、実際コンパイルエラーを吐き出しません。ただ、Playgroundで試してみると一応Warningは表示されます。

実際にコードで確認してみます。
エクステンション側でデフォルト実装を施している場合、その内部でプロトコル側に書かれるはずのメソッドが呼び出される形になります。けれど、記事で問題にされているように、プロトコルにメソッドを書き漏らしてしまった場合どうなるでしょうか。

「#書き方」の項のコードをコメントアウトして書くと、こういった形になるかと思います。

protocol TestProtocol {
    //コメントアウト
    //func test(a: Int, b: Int) 
}

extension TestProtocol {
    //② デフォルト実装(規定実装)
    func test(a: Int, b: Int = 15) {
	//Swift Compiler Warning
	//Function call causes an infinite recursion
        self.test(a: a, b: b) 
    }
}

警告で示されているように、メソッドが無限で呼び出される形になります。ただ警告だけで、コンパイル自体は通ってしまうわけで、恐ろしくはある...

ちなみに、これ名前を変えるとどうなるかというと

protocol TestProtocol {
    //コメントアウト
    //func testA(a: Int, b: Int) 
}

extension TestProtocol {
    //② デフォルト実装(規定実装)
    func testB(a: Int, b: Int = 15) {
	//Swift Compiler Error
	//Cannot find 'testA' in scope
        self.testA(a: a, b: b) 
    }
}

というよくみるコンパイルエラーになります。だから厳密に言えば、同一の命名でデフォルト実装を行うと、どうも完全に別個のメソッドであるわけではなさそうです。

参考

https://yamatooo.blog/entry/2021/07/30/083000

https://qiita.com/nakagawa1017/items/18ff5b040957cba8e7e5

Discussion