Sponsor Link
環境&対象
- macOS Big Sur 11.3
- Xcode 12.5
- iOS 14.5
RingBuffer
以下の記事で、Generics を使用して、様々なタイプに使える RingBuffer を作りました。
[Swift] Generics を使って RingBuffer を作る
追加したい機能 findIndex(of:)
指定した値が、Buffer に含まれているのか、含まれているならその インデックスを返すようにしてみます。
テストコード : 仕様
以下のように使える機能とします。
func test_find_findExistingElement_ShouldBeFound() throws {
var sut = RingBuffer<String>(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)
}
- World は、2番目に write されたので、Index としては、2が返されるはず
- Aloha という値は存在しないので、Index としては、nil が返される
実装 findIndex(of:) の基本実装
実装をすすめていくと以下のようになります。
実装内容自体は、oldestIndex – latestIndex 間の要素をチェックして、一致すればその時のインデックスを返す処理を行なっています。
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
}
実装していくと問題が発生します。Generics で T というタイプを処理していますが、T の比較方法がわからないのです。 とりあえず、== と書いてますが、比較できることはだれも保証してくれないです。
実装 findIndex(of:) へ制約追加
このような時に、タイプ T に条件を追加することができるようになっています。
// (1)
func findIndex(of valueToFind:T) -> Int? where T:Equatable {
if count == 0 { return nil }
for index in oldestIndex...latestIndex {
guard let value = self[index] else { continue }
if value == valueToFind {
return index
}
}
return nil
}
- func 定義の後に、where 節を使用して、タイプ T に対しての制約を追加しています。
つまり、findIndex(of:) は T が Equatable に conform している時のみ使えるメソッドになります。
where 節の効果検証
Int に対しての RingBuffer を作ってから、findIndex(of:) を使ってみます。
var ringInt = RingBuffer<Int>(capacity: 4)
let intIndex = ringInt.findIndex(of: 3)
問題なく使えます。これは、Int が Equatable に conform しているためです。
次に、独自の struct を定義して、その struct を要素に持つ RingBuffer を定義してみます。
struct MyData {
var int:Int
}
var ring = RingBuffer<MyData>(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<MyData>(capacity: 4)
let index = ring.findIndex(of: MyData(int: 4)) // compile ok
- MyData を Equtable に conform させるために、追加
含まれるプロパティ全てが一致すると一致と判定されます。
struct 定義での placeholder にも追加できます
関数の定義時ではなく、struct 定義の時の placeholder に制約を追加することもできます。
// (1)
public struct RingBuffer<T: Equatable> {
// ...
// 省略
// ...
// (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
}
}
- struct の placeholder に制約を追加するにはこのように記述します
- struct 宣言時に T に対して制約を設定しているので、関数レベルでの制約記述は不要です
このように制約を struct に付与する時には、以下に気をつけなければいけません。
- struct (この場合 RingBuffer) が保持できる要素は、Equatable に conform する必要が発生します
つまり、関数宣言時に制約を付与した時には、その制約を満たさない時に使えないのは、関数だけでしたが、
struct 宣言時に制約を付与すると、その制約を満たさないと struct 自体が使えなくなります。
まとめ:Generics のタイプに制約を付与する
- struct 宣言時の placeholder にも制約を付与することができる
- struct 宣言時に制約を付与すると、制約を満たさないタイプは、struct が使用できなくなる
- 関数宣言直後に、where 節で制約を付与することができる
- 関数宣言で制約を付与すると、制約を満たさないタイプではその関数が使用できなくなる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link