swift-concurrency-extrasのAnyHashableSendableについて
TCA Advent Calendar 2024の記事です。
はじめに
TCAに使われているswift-concurrency-extrasのAnyHashableSendableについて解説します。バージョンはv1.3です。
もし何か間違いがあったらコメントいただければ助かります。
AnyHashableSendableについて
めちゃくちゃ雑にいうと、任意のHashableかつSendableな値をラップするための構造体です。
Swiftでは、構造体AnyHashable を使用して任意のHashable値を型消去できます。
しかしAnyHashableはSendableに準拠していません。かつ、HashableプロトコルもSendableに準拠していません。AnyHashableSendableはこの制限を回避しつつ、並行性に対応したSendableなラッパーを提供します。
テスト
testAnyHashable
AnyHashable型がキャストできるかのテストをしているっぽいです。
func testAnyHashable() {
XCTAssertEqual(AnyHashableSendable(1), 1 as AnyHashable)
}
testExistential
HashableにもSendableにも準拠している値をOptionalのメソッドまpでAnyHashableSendableに変換し、その結果がAnyHashableSendableに変換できたかをテストしてます。
func testExistential() {
// 任意の Hashable かつ Sendable な値を保持するオプショナル変数を作成
let base: (any Hashable & Sendable)? = 1
// `base` が非 nil なら `AnyHashableSendable` にラップする
let wrapped = base.map(AnyHashableSendable.init)
// ラップされた値が期待通り `AnyHashableSendable(1)` と等しいことを検証
XCTAssertEqual(wrapped, AnyHashableSendable(1))
}
Optionalにしているのは、
SwiftにはOptionalのmapメソッドがあるからです。
これはnilでなければ実行されます。
testBasics
AnyHashableSendableの値が同じなら等価で、そうでないなら等価じゃないというテストと、ネストされたAnyHashableSendableが再度ラップされてAnyHashableSendable(AnyHashableSendable(1))
も、元のAnyHashableSendable(1)
と等価であることを検証します。
func testBasics() {
XCTAssertEqual(AnyHashableSendable(1), AnyHashableSendable(1))
XCTAssertNotEqual(AnyHashableSendable(1), AnyHashableSendable(2))
// 任意の Hashable かつ Sendable な値を
// `AnyHashableSendable` に変換するヘルパー関数を定義
func make(_ base: some Hashable & Sendable) -> AnyHashableSendable {
AnyHashableSendable(base)
}
let flat = make(1)
let nested = make(flat)
XCTAssertEqual(flat, nested)
}
testLiterals
AnyHashableSendableさまざまなリテラル(ブール値、整数、浮動小数点数、文字列)から正しく初期化され、期待通りに動作することを検証しています。
func testLiterals() {
// XCTAssertEqualは異なる型を比較できる。
// AnyHashableSendableの`==`で右辺をAnyHashableSendableにできる。
XCTAssertEqual(AnyHashableSendable(true), true)
XCTAssertEqual(AnyHashableSendable(1), 1)
XCTAssertEqual(AnyHashableSendable(4.2), 4.2)
XCTAssertEqual(AnyHashableSendable("Blob"), "Blob")
// リテラルを使用して `AnyHashableSendable` を直接初期化
let bool: AnyHashableSendable = true
// `base` プロパティが正しくラップされていることを検証
XCTAssertEqual(bool.base as? Bool, true)
// 型キャスト後の等価性を検証
XCTAssertEqual(bool as AnyHashable as? Bool, true)
}
おそらくですが、
XCTAssertEqual(AnyHashableSendable(1), 1)
は左辺をネストされたAnyHashableSendable(AnyHashableSendable(1))
とし、右辺をAnyHashableSendable(1)
としているのではないかと思います。
おわりに
TCAではID型を使う際に利用されているようです。
Discussion