[Swift] Generics の型に制約をつけて拡張する

Swift

Generics を使用して作った RingBuffer を、制約条件を追加しながら機能拡張してみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

RingBuffer

以下の記事で、Generics を使用して、様々なタイプに使える RingBuffer を作りました。

Swift[Swift] Generics を使って RingBuffer を作る

追加したい機能 findIndex(of:)

指定した値が、Buffer に含まれているのか、含まれているならその インデックスを返すようにしてみます。

テストコード : 仕様

以下のように使える機能とします。


    func test_find_findExistingElement_ShouldBeFound() throws {
        var sut = RingBuffer(capacity: 4)
        sut.write("Hello")
        sut.write(",")
        sut.write("World")
        sut.write("!")

        // (1) 
        XCTAssertEqual(try XCTUnwrap(sut.findIndex(of: "World")), 2)
        // (2)
        XCTAssertEqual(sut.findIndex(of: "Aloha"), nil)
    }

コード解説
  1. World は、2番目に write されたので、Index としては、2が返されるはず
  2. Aloha という値は存在しないので、Index としては、nil が返される

実装 findIndex(of:) の基本実装

実装をすすめていくと以下のようになります。

実装内容自体は、oldestIndex - latestIndex 間の要素をチェックして、一致すればその時のインデックスを返す処理を行なっています。


    func findIndex(of valueToFind:T) -> Int? {
        for index in oldestIndex...latestIndex {
            guard let value = self[index] else { continue }
            if value == valueToFind {
                return index
            }
        }
        return nil
    }

実装していくと問題が発生します。Generics で T というタイプを処理していますが、T の比較方法がわからないのです。 とりあえず、== と書いてますが、比較できることはだれも保証してくれないです。

実装 findIndex(of:) へ制約追加

このような時に、タイプ T に条件を追加することができるようになっています。


    // (1)
    func findIndex(of valueToFind:T) -> Int? where T:Equatable {
        for index in oldestIndex...latestIndex {
            guard let value = self[index] else { continue }
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
コード解説
  1. func 定義の後に、where 節を使用して、タイプ T に対しての制約を追加しています。
    つまり、findIndex(of:) は T が Equatable に conform している時のみ使えるメソッドになります。

where 節の効果検証

Int に対しての RingBuffer を作ってから、findIndex(of:) を使ってみます。


var ringInt = RingBuffer(capacity: 4)
let intIndex = ringInt.findIndex(of: 3)

問題なく使えます。これは、Int が Equatable に conform しているためです。

次に、独自の struct を定義して、その struct を要素に持つ RingBuffer を定義してみます。


struct MyData {
    var int:Int
}

var ring = RingBuffer(capacity: 4)
let index = ring.findIndex(of: MyData(int: 4)) // compile error Instance method 'findIndex(of:)' requires that 'MyData' conform to 'Equatable'

エラーメッセージが全てを説明していますが、MyData が Equatable に conform していないから、findIndex(of:) が使えないと言うエラーです。

なお、シンプルな struct であれば、Equatable に準拠していると宣言するだけで、Equtable に conform させることができます。
ですので、以下のように "extension MyData: Equatable {}" と追加するとエラーはなくなります。


struct MyData {
    var int:Int
}
// (1)
extension MyData: Equatable {}
var ring = RingBuffer(capacity: 4)
let index = ring.findIndex(of: MyData(int: 4)) // compile ok
コード解説
  1. MyData を Equtable に conform させるために、追加
    含まれるプロパティ全てが一致すると一致と判定されます。

struct 定義での placeholder にも追加できます

関数の定義時ではなく、struct 定義の時の placeholder に制約を追加することもできます。


// (1)
public struct RingBuffer {
    // ...
    // 省略
    // ...

    // (2)
    func findIndex(of valueToFind:T) -> Int? {
        if count == 0 { return nil }
        for index in oldestIndex...latestIndex {
            guard let value = self[index] else { continue }
            if value == valueToFind {
                return index
            }
        }
        return nil
    }
}
コード解説
  1. struct の placeholder に制約を追加するにはこのように記述します
  2. struct 宣言時に T に対して制約を設定しているので、関数レベルでの制約記述は不要です

このように制約を struct に付与する時には、以下に気をつけなければいけません。

  • struct (この場合 RingBuffer) が保持できる要素は、Equatable に conform する必要が発生します

つまり、関数宣言時に制約を付与した時には、その制約を満たさない時に使えないのは、関数だけでしたが、
struct 宣言時に制約を付与すると、その制約を満たさないと struct 自体が使えなくなります。

まとめ:Generics のタイプに制約を付与する

Generics のタイプに制約を付与する
  • struct 宣言時の placeholder にも制約を付与することができる
  • struct 宣言時に制約を付与すると、制約を満たさないタイプは、struct が使用できなくなる
  • 関数宣言直後に、where 節で制約を付与することができる
  • 関数宣言で制約を付与すると、制約を満たさないタイプではその関数が使用できなくなる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です