📝

[Swift] 異なるProtocolが同名のメソッドを持ち単一のクラスが両方のプロトコルに準拠したい場合

2023/05/01に公開

はじめに

この記事ではSwiftで複数のprotocolが同名のメソッドを持つ場合にどのようにすれば良いかのメモです。この記事では_から始まる属性を使用しているので実プロダクトでは使用しない方が良いと思います。知見として面白いのでメモにしました。

TL;DR

@_implementsを使うことで同名のメソッドを持つprotocolを単一のクラスで実装することができます。

コード例

例えばAというプロトコルがconfigureというメソッドを宣言しているとします。また他にもBというプロトコルがconfigureというメソッドを宣言しているとします。

protocol A {
    func configure()
}

protocol B {
    func configure()
}

上記のABというプロトコルの両方にあるクラスCが準拠する必要がある場合、メソッド名が衝突して困ります。


protocol A {
    func configure()
}

protocol B {
    func configure()
}

class C: A, B {
    func configure() {
        // コンパイルが通るが、AとBの実装を分けることができない
        print("cofigured!")
    }
}

解決策

@_implements属性を用いることでクラスが実装するメソッドのシグネチャとprotocolのシグネチャを分けることができます。

上記のAとBの実装をC側で別々にしたい場合は以下のように書くことができます。
@_implements("プロトコル名", "メソッドのシグネチャ")と書けば良いです。(例: @_implements(B, configure())

protocol A {
    func configure()
}

protocol B {
    func configure()
}

class C: A, B {
    @_implements(A, configure())
    func configureA() {
        print("A is cofigured!")
    }

    @_implements(B, configure())
    func configureB() {
        print("B is configured!")
    }
}

ケーススタディ

あまり実用的な例はありませんが、考え方としては同名の関数が複数のプロトコルで定義されている場合に有効です。
例えばIDを生成する責務を持つクラスがあったとして、その実装を要求するプロトコルにRandomIDGeneratorIncrementalIDGenratorがあったとします。RandomIDGeneratorは順番に関係なくランダムにIDを生成しますが、IncrementalIDGeneratorは数字を1つずつインクリメントしてIDを生成します。

protocol IncrementalIDGenerator {
    func make() -> String
}

protocol RandomIDGenerator {
    func make() -> String
}

class IDGenerator: IncrementalIDGenerator, RandomIDGenerator {

    private var count: Int = 0

    @_implements(IncrementalIDGenerator, make())
    func makeIncremental() -> String {
        count += 1
        let formatter = NumberFormatter()
        formatter.minimumIntegerDigits = 32
        return formatter.string(for: count)!
    }

    @_implements(RandomIDGenerator, make())
    func makeRandom() -> String {
        count += 1
        return (0..<32)
            .map { _ in Int.random(in: 0...9) }
            .map { String($0) }
            .joined(separator: "")
    }
}

大抵の場合はmakeRandommakeIncrementalをプロトコルで分けるのではなく実体(class)で分けることが多いと思いますが、実体を同じにすることでcountという変数を共有できました。(他にもやり方はあると思います)

RandomIDGenerator IncrementalIDGenerator

注意

下のURLにあるように_から始まる属性は使うことを非推奨されているので使わない方が良いと思いますが、知見としてメモをするべく記事にしました。

https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md

Discussion