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

15752 ワード

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内部での処理の仕方が異なっている可能性があります。正直、この辺りはわかっていません。

一応の注意

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