はじめに (対象読者・この記事でわかること)
この記事は、Swiftでジェネリクスプログラミングを学び始めた中級者以上の開発者を対象にしています。特に、DictionaryやSetを使ったジェネリクスなコードを書いていて「なぜかHashableを要求しているのに値が比較できない」という状況に悩んでいる方に最適です。
この記事を読むことで、Swiftのプロトコル体系がEquatableとHashableの関係を正確に理解し、ジェネリクス型に安全に比較処理を実装できるようになります。実装例を通じて、型パラメータに適切な制約を与える方法と、associated typeがもたらす落とし穴を回避するテクニックを身につけられます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Swiftの基本的な文法と型システム - ジェネリクス(汎用関数・汎用型)の使い方 - プロトコルと拡張の基礎
SwiftのHashableとEquatableの関係を整理する
Swift標準ライブラリではHashableがEquatableを継承しているため、理論上は「Hashableを要求すれば自動的に==も使えるはず」です。しかし実際にはジェネリクス関数内で==を書こうとしてもコンパイルエラーになるケースが頻発します。これは、型パラメータにHashable制約を課しても、associated type Selfのスコープが想定外に振る舞うためです。要するに「プロトコルが継承している」ことと「型パラメータがその制約を満たす」ことは別物ということです。
ジェネリクス比較でつまずく実装パターンと回避策
ここでは実際に遭遇する3つの失敗パターンと、それぞれの解決法を示します。
パターン1: 型パラメータにHashableを要求しても==が使えない
Swift// ❌ コンパイルエラー func hasDuplicate<T: Hashable>(_ items: [T]) -> Bool { var seen = Set<T>() for item in items { if seen.contains(item) { return true } // OK // if seen.contains(where: { $0 == item }) { ... } // エラー } return false }
Set<T>.contains(_:)はTがHashableであれば問題なく動きますが、クロージャ内で==を書こうとすると「T同士を比較する方法がわからない」と怒られます。これはTがSelfと一致するときだけ==が使えるためです。
解決策: where句で同一性を保証する
Swift// ✅ コンパイル通る func hasDuplicate<T: Hashable>(_ items: [T]) -> Bool where T: Equatable // 冗長だが明示 { var seen = [T]() for item in items { if seen.contains(where: { $0 as AnyObject === item as AnyObject }) { return true } seen.append(item) } return false }
where T: Equatableを付けてもHashableの継承関係で既にEquatableですが、ジェネリクスコンテキストで再宣言することでコンパイラに「このスコープではT自身をEquatableとして扱ってよい」と教えています。
パターン2: プロトコル境界でassociated typeが制約を満たさない
Swiftprotocol Item { associatedtype ID: Hashable var id: ID { get } } func compare<ID: Hashable>(left: any Item, right: any Item) -> Bool where left.ID == right.ID // エラー: 'left' is not a type { left.id == right.id // 両方ともHashableなのに==が使えない }
any Itemはexistentialで、associated typeが具象化されていないため==が使えません。
解決策: 型パラメータにプロトコルを据える
Swift// ✅ 正しい設計 func compare<ID: Hashable, I1: Item, I2: Item>( left: I1, right: I2 ) -> Bool where I1.ID == I2.ID, I1.ID: Equatable { left.id == right.id // OK }
existentialではなく具象型パラメータにすることで、associated typeが同一のIDとして扱われ、Equatable制約が継承により満たされます。
パターン3: ジェネリック構造体でHashableを自動実装するときの落とし穴
Swiftstruct Pair<T: Hashable> { let a, b: T } extension Pair: Hashable {} // 自動合成 // このときTがEquatableでなくてもHashableとして振る舞うが、 // Pair同士を==で比較しようとするとTの==が必要になる
Pair<T>はT: HashableのおかげでHashableを自動実装できますが、実際にpair1 == pair2と書こうとするとTもEquatableでなければなりません。これは自動合成された==がT.==を呼ぶためです。
解決策: 型パラメータにEquatableを要求する
Swiftstruct Pair<T: Hashable & Equatable> { // 合成に必要 let a, b: T } extension Pair: Hashable {} // 自動合成で==も生成される
Hashableを継承しているEquatableを明示的に要求しておけば、利用者側が「Hashableさえあればいいや」と油断することなく、比較処理まで安全に使えます。
まとめ
本記事では、Swiftでジェネリクス型にHashableを要求しても==が使えない理由を、associated typeとexistentialの関係から解説しました。
HashableはEquatableを継承しているが、型パラメータのスコープでは再宣言が必要any Protocolはassociated typeを具象化しないため、比較演算子が使えない- 型パラメータに
& Equatableを要求しておくことで、予期せぬコンパイルエラーを回避
この記事を通して、Swiftの型システムが「プロトコルが持つ制約」と「ジェネリクスが要求する制約」を別物として扱う設計を理解できたはずです。 次回は、Swift 5.7で導入された「primary associated type」を活用して、より簡潔にexistentialを扱う方法を紹介します。
参考資料
- The Swift Programming Language: Generics
- SE-0143: Conditional Conformances
- Swift Protocols: From Existential to Generic Constraints
- Hashable / Equatable: When and How
