「Hashable」について…

Swift

Swift でアプリを作るとき、よく登場するのが Hashable
この記事では、Hashable が果たす役割、Equatable との関係を具体例を交えて丁寧に解説します。


Hashableとは?

Hashable は、値から「ハッシュ値」と呼ばれる数値を計算できることを表すプロトコルです。ハッシュ値は、値を一意に区別するための「指紋」のようなものだとイメージするとわかりやすいです。

Swift の定義としては、Hashable は Equatable を継承しており、次のようなイメージです:

protocol Hashable : Equatable {
    func hash(into hasher: inout Hasher)
}

つまり、Hashable に準拠する型は:

  • 等価比較(==)ができる(Equatable)
  • ハッシュ値を計算できる(Hashable)

という二つの性質を持つことになります。


なぜHashableが必要なのか?

Hashable が本領を発揮するのは、次のような場面です。

  • Set<T> の要素として使うとき
  • Dictionary<Key, Value> の Key として使うとき
  • 高速に「同じ値かどうか」比較したいとき
  • SwiftUI が内部で差分計算(Diffing)を行うとき

例えば、ユーザーを一意に管理したい場合、Set を使うと便利です:

struct User: Hashable {
    let id: String
    let name: String
}
var users: Set<User> = []
users.insert(User(id: "1", name: "Taro"))
users.insert(User(id: "1", name: "Taro"))  // 同じUserなので重複しない

このとき、Set は内部で:

  • ユーザーのハッシュ値を計算する
  • 既に同じハッシュ値+同じ内容の要素があるかチェックする

という処理を行っています。これが、Hashable が「高速比較」に向いていると言われる理由です。


Hashable と Equatable の違い

よく混同されやすいのが EquatableHashable です。

  • Equatable:2つの値が「等しいかどうか」を判定できる(== が使える)
  • Hashable:Equatable の上に、「ハッシュ値を計算できる」性質が追加されたもの

つまり、Hashable に準拠している型は、Equatable としても扱えます。

let a = User(id: "1", name: "Taro")
let b = User(id: "1", name: "Taro")
if a == b {
    print("同じユーザー")
}

この == による比較が成り立つことが、Hashable を正しく機能させる前提条件になっています。


Hashable で本来必要な記述

struct を Hashable に準拠させる場合、本来は次のようなコードを書く必要があります:

struct Post: Hashable {
    let id: String
    let authorId: String
    let text: String
    let createdAt: Date

    // 本来は hash(into:) を実装する必要がある
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(authorId)
        hasher.combine(text)
        hasher.combine(createdAt)
    }

    // Equatable も同時に実装する必要がある
    static func == (lhs: Post, rhs: Post) -> Bool {
        lhs.id == rhs.id &&
        lhs.authorId == rhs.authorId &&
        lhs.text == rhs.text &&
        lhs.createdAt == rhs.createdAt
    }
}

しかし Swift の struct は、次の2つの条件を満たしていれば、これらの実装を 自動で生成(自動合成)してくれます:

  • すべての stored property が Hashable に準拠している
  • hash(into:)== を自分で実装していない

そのため実際には、次のように書くだけで HashableEquatable が自動で付いてきます:

struct Post: Hashable {
    let id: String
    let authorId: String
    let text: String
    let createdAt: Date
}

SwiftUI と Hashable の関係

SwiftUI の文脈では、Hashable は主に「差分計算」と「コレクション操作」のために使われます

  • List や ForEach が内部でモデルを比較する
  • 状態の変化に応じて UI を最小限だけ更新する
  • アニメーションやトランジションの対象を特定する

実際のコードとしては、Identifiable と組み合わせて次のような定義になることが多いです:

struct Post: Identifiable, Hashable {
    let id: String
    let authorId: String
    let text: String
    let createdAt: Date
}
struct TimelineView: View {
    let posts: [Post]
    var body: some View {
        List(posts) { post in
            Text(post.text)
        }
    }
}

ここで Hashable に準拠していることで、SwiftUI が内部で差分を効率的に計算できるようになり、タイムラインの更新がスムーズかつ高速になります。


Hashableを自前で実装するケース

ほとんどのケースでは自動合成で十分ですが、次のようなケースでは自前で hash(into:) を実装することもあります。

  • 特定のプロパティだけを比較・ハッシュ対象にしたい
  • 巨大な構造体でハッシュ計算コストを抑えたい
struct User: Hashable {
    let id: String
    let name: String
    let profileText: String
    func hash(into hasher: inout Hasher) {
        // id だけをハッシュの対象にする
        hasher.combine(id)
    }
    static func == (lhs: User, rhs: User) -> Bool {
        lhs.id == rhs.id
    }
}

このように、「何をもって同一とみなすか」を制御したい場合には、Hashable と Equatable を手動実装することがあります。ただし、基本的には自動合成で問題ないケースが多いため、必要性が出てきたときにだけ検討するくらいで十分です。


まとめ

  • Hashable は「ハッシュ値を計算できる」という性質を表すプロトコル
  • Set の要素、Dictionary のキーとして使うために必要
  • 内部的には Equatable(==で比較可能)でもある
  • 多くの struct は自動合成で Hashable を満たせる
  • SwiftUI の差分更新やモデル比較にも活用される
  • Identifiable(id による識別)と組み合わせると、リストUIとの相性が非常に良い

「とりあえず Hashable を付ける」から一歩進んで、
「なぜ Hashable が必要なのか」「どんな場面で効いているのか」を理解しておくと、モデル設計や SwiftUI の挙動がぐっとクリアに見えてきます。