SwiftのHashableの理解と活用12選

SwiftのHashableプロトコルを学ぶキーボードとスクリーンSwift
この記事は約16分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Swiftのプログラミングの中で、データのユニークな表現や、高速なデータの検索、辞書型のキーとしての利用など、さまざまな場面でHashableプロトコルが活躍します。

この記事では、SwiftのHashableプロトコルの基本から応用まで、12の詳細なサンプルコードと解説を通じて、Hashableの理解と活用を深めていきます。

特に初心者の方でも、この記事を読めばHashableの全てを理解できるように、徹底的に解説しています。

プログラミングのスキルアップを目指す皆様、是非最後までお付き合いください。

●SwiftのHashableとは

SwiftにおけるHashableは、プロトコルの一つです。

このプロトコルを採用することで、オブジェクトに一意のハッシュ値を与えることが可能となります。

ハッシュ値は、データの内容から導き出される固有の数値で、これによってデータの同一性やユニーク性を判定することができます。

Hashableを採用した型は、辞書型のキーとして利用できるという特徴も持っています。

これは、辞書型においてキーの値がユニークである必要があるため、Hashableのユニークなハッシュ値が役立つからです。

また、HashableはEquatableプロトコルを継承しています。

これにより、Hashableを採用した型のインスタンス間での等価性比較が可能となります。

具体的には、== 演算子を使用して二つのインスタンスが等しいかどうかを判定することができます。

○Hashableの基本概念

Hashableは、主に二つの要件を持っています。

それは、hash(into:)メソッドと、==演算子のオーバーロードです。

□hash(into:)メソッド

このメソッドを使用して、オブジェクトのハッシュ値を計算します。

Swiftの標準ライブラリには、多くの基本型がこのメソッドを持っており、カスタム型を作成する際にもこのメソッドを実装することでHashableを採用することができます。

□==演算子のオーバーロード

Equatableプロトコルを継承しているため、Hashableを採用した型は==演算子を使用して等価性の比較を行うことができます。

この演算子をカスタマイズすることで、独自の等価性の基準を定義することも可能です。

●Hashableの使い方

Swiftでのプログラミングにおいて、データの管理や操作を効率的に行うために、Hashableプロトコルは非常に重要な役割を果たしています。

ここでは、Hashableの基本的な使い方を解説していきます。

○サンプルコード1:基本的なHashableの実装

Swiftでは、特定のオブジェクトや値がハッシュ可能であることを示すために、Hashableプロトコルを採用します。

このプロトコルを実装することで、そのオブジェクトや値を辞書のキーとして使用することができるようになります。

ここでは、シンプルな構造体Personを定義し、Hashableを実装した例を紹介します。

struct Person: Hashable {
    var name: String
    var age: Int
}

このコードでは、Personという構造体を定義しています。

この例では、nameageという2つのプロパティを持っています。

Hashableプロトコルを採用することで、Person型のオブジェクトがハッシュ可能となり、辞書のキーとして使用できるようになります。

このコードを実行すると、Person型のオブジェクトをハッシュ可能として扱うことができるようになります。

○サンプルコード2:カスタム型でのHashableの利用

カスタム型でHashableを利用する場合、独自のハッシュ関数を提供することが可能です。

ここでは、Bookという構造体を定義し、Hashableをカスタマイズして実装した例を紹介します。

struct Book: Hashable {
    var title: String
    var author: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
        hasher.combine(author)
    }
}

このコードでは、Bookという構造体を定義しています。

この例では、titleauthorという2つのプロパティを持っています。

hash(into:)メソッドを使用して、ハッシュ値を計算する方法をカスタマイズしています。

このコードを実行すると、Book型のオブジェクトのハッシュ値は、titleauthorの組み合わせに基づいて計算されるようになります。

○サンプルコード3:複雑なデータ構造でのHashableの適用

さらに複雑なデータ構造にHashableを適用する場合も考えられます。

ここでは、ネストされた構造体を持つ例として、Libraryを定義し、その中にBookのリストを持つように実装した例を紹介します。

struct Library: Hashable {
    var name: String
    var books: [Book]

    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(books)
    }
}

このコードでは、Libraryという構造体を定義しており、nameという文字列のプロパティと、Bookのリストを持つbooksプロパティを持っています。

hash(into:)メソッドを使用して、ハッシュ値の計算方法をカスタマイズしています。

このコードを実行すると、Library型のオブジェクトのハッシュ値は、namebooksの組み合わせに基づいて計算されるようになります。

●Hashableの応用例

Hashableプロトコルを理解したら、それをどのように実際のプログラムで活用できるのかを理解することが非常に重要です。

Hashableを使うことで、データの高速検索や一意なデータリストの作成、辞書型でのキーとしての利用など、さまざまな利点を享受できます。

○サンプルコード4:Hashableを用いたデータの高速検索

Hashableを実装した型は、そのハッシュ値によってデータを高速に検索することができます。

例として、下記のコードでは、Hashableを実装したStudent型を用いて、Setから特定の学生を高速に検索しています。

struct Student: Hashable {
    var id: Int
    var name: String
}

let student1 = Student(id: 1, name: "太郎")
let student2 = Student(id: 2, name: "花子")
let studentSet: Set<Student> = [student1, student2]

let targetStudent = Student(id: 1, name: "太郎")
if studentSet.contains(targetStudent) {
    print("太郎は学生セットに存在します。")
}

このコードでは、Student型を作成し、それを元に太郎と花子という2名の学生をSetに追加しています。

そして、太郎がそのSetに含まれているかどうかを高速に確認することができます。

Setはハッシュ値を利用してデータの存在を確認するので、データの量が増えても検索速度は非常に高速です。

この例から、ハッシュセットを使うことで、大量のデータから特定のデータを瞬時に取得できることがわかります。

○サンプルコード5:一意なデータのリストの作成

Hashableを活用すれば、一意のデータだけを含んだリストを作成することも簡単です。

下記の例では、複数の重複する文字列から一意な文字列だけを取り出し、新しい配列を作成しています。

let names = ["田中", "鈴木", "田中", "佐藤", "鈴木"]
let uniqueNames = Array(Set(names))
print(uniqueNames)

このコードでは、namesという配列には重複する名前が含まれていますが、Setを利用して一意の名前だけを抽出し、それを新しい配列uniqueNamesに格納しています。

この例から、Setを使うことで、簡単に重複を取り除いた一意なリストを作成できることがわかります。

○サンプルコード6:辞書型でのキーとしての利用

Hashableを実装した型は、Swiftの辞書型でキーとして利用できます。

下記のコードでは、CustomKey型を作成し、それを辞書のキーとして使用しています。

struct CustomKey: Hashable {
    var value1: Int
    var value2: String
}

var dictionary: [CustomKey: String] = [:]

let key1 = CustomKey(value1: 1, value2: "A")
dictionary[key1] = "データ1"
print(dictionary[key1] ?? "キーに対応するデータはありません。")

このコードでは、CustomKeyという型を辞書のキーとして使用しています。

Hashableを実装しているため、このようなカスタム型をキーとして辞書に利用することが可能です。

●Hashableの詳細な使い方

Hashableは、Swiftのデータ型がハッシュ可能であることを表すプロトコルです。

ハッシュ可能とは、そのデータ型が固有のハッシュ値を持ち、それによってその型のインスタンスが他のインスタンスと区別できるようになることを意味します。

○サンプルコード7:hash(into:)メソッドのカスタマイズ

SwiftのHashableプロトコルを採用すると、デフォルトでのハッシュ関数が提供されます。

しかし、特定のケースでカスタムハッシュ関数を利用したい場合は、hash(into:)メソッドをオーバーライドすることで実現できます。

このコードでは、Personという構造体を定義し、Hashableプロトコルを採用しています。

名前と年齢を属性として持ち、カスタマイズしたhash(into:)メソッドを通じて、特定のハッシュ計算を実装しています。

struct Person: Hashable {
    let name: String
    let age: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(age)
    }
}

この例では、name属性とage属性をcombineメソッドを使用してハッシュ計算に組み込んでいます。

このようにして、Personの各インスタンスには固有のハッシュ値が割り当てられます。

hash(into:)メソッドのカスタマイズにより、ハッシュ計算の精度を向上させることができます。

これによって、データの整理や検索の効率が向上することが期待できます。

○サンプルコード8:==演算子のオーバーロード

Hashableを実装する際、== 演算子のオーバーロードも考慮することが重要です。

これは、2つのオブジェクトが等しいかどうかを判断するためのものです。

このコードでは、上述のPerson構造体を利用して、2つのPersonインスタンスが等しいかどうかを判断する==演算子をオーバーロードしています。

extension Person {
    static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

この例では、2つのPersonインスタンスのname属性とage属性がそれぞれ一致する場合、その2つのインスタンスは等しいと判断されます。

●注意点と対処法

SwiftのHashableプロトコルは非常に便利なツールですが、使用する際にはいくつかの注意点があります。

正しく理解し、効果的に活用するための注意点と対処法を以下に詳しく説明します。

○Hashableの常識的な制約

Hashableプロトコルを実装するときに最も重要なことは、ハッシュ値が同じであればそのオブジェクトも等しいとは限らないということです。

逆に、ハッシュ値が異なればそのオブジェクトは絶対に等しくないという保証があります。

この点を理解しておくことで、多くの誤解や問題を回避することができます。

また、Hashableを実装するオブジェクトの状態が変わったとき、ハッシュ値も変わる可能性があるため、注意が必要です。

特に、コレクション内でオブジェクトの状態を変更すると、予期しない動作やバグの原因となります。

○サンプルコード9:一般的な落とし穴とその解消方法

このコードでは、Hashableプロトコルを実装したカスタム型において、一般的に遭遇する可能性のある問題と、それを解消する方法を表しています。

この例では、プロパティの変更によってハッシュ値が変動する問題を取り上げます。

struct Person: Hashable {
    var name: String
    var age: Int
}

var personSet = Set<Person>()
let john = Person(name: "John", age: 25)
personSet.insert(john)

// ここでJohnの年齢を変更
var mutableJohn = john
mutableJohn.age = 26

// この時点で、personSet内のJohnのハッシュ値は変動していません

このコードでは、Person型のオブジェクトの年齢を変更しても、ハッシュ値は変動しないため、Set内での位置や検索に影響を与えないことを表しています。

しかし、実際にオブジェクトの状態を変更する際には、そのオブジェクトがコレクション内で変動しないように注意が必要です。

この問題を回避するための方法として、ハッシュ値の計算に使用するプロパティを変更不可(let)にすることが考えられます。

これにより、オブジェクトの状態が変わることなく、ハッシュ値も安定して保持されることになります。

上記のコードを使って、オブジェクトのハッシュ値が変動しないことを確認すると、Johnの年齢が25から26に変更されても、Set内での位置や検索に影響を与えないことがわかります。

●カスタマイズ方法

Hashableプロトコルは、Swiftの中核的なプロトコルの1つです。

そのため、デフォルトの動作だけでなく、特定のニーズに合わせてカスタマイズする方法も提供されています。

ここでは、Hashableをカスタマイズするさまざまな方法を詳細に解説していきます。

○サンプルコード10:カスタムHasherの実装

Hashableのhash(into:)メソッドは、Hasherという構造体を使用してオブジェクトのハッシュ値を計算します。

独自のハッシュアルゴリズムを実装したい場合は、このHasherをカスタマイズすることが可能です。

struct CustomHasher: Hasher {
    var seed: Int

    init(seed: Int) {
        self.seed = seed
    }

    mutating func combine(_ value: Int) {
        // こちらは簡単なハッシュ関数の例です
        seed ^= value &+ 0x9e3779b9 &+ (seed << 6) &+ (seed >> 2)
    }

    mutating func finalize() -> Int {
        return seed
    }
}

struct MyData: Hashable {
    let value: Int

    func hash(into hasher: inout Hasher) {
        var customHasher = CustomHasher(seed: value)
        customHasher.combine(value)
        hasher.combine(customHasher.finalize())
    }
}

このコードでは、CustomHasherという独自のHasherを作成しています。

この例では、seedを初期化し、そのseedを基に独自のハッシュ関数を実装しています。

その後、MyData構造体でこのカスタムHasherを利用してhash(into:)メソッドをオーバーライドしています。

このようなコードを実行すると、MyDataのインスタンスはカスタムHasherを使用してハッシュ値を計算することになります。

○サンプルコード11:特定のプロパティをハッシュから除外する

時折、特定のプロパティをハッシュ計算から除外したい場合があります。

ここでは、そのようなケースを考慮したサンプルコードの例を紹介します。

struct Person: Hashable {
    let id: Int
    let name: String
    let age: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        // nameとageはハッシュ計算から除外
    }
}

このコードでは、Person構造体にはid、name、ageの3つのプロパティがありますが、hash(into:)メソッドではidだけをハッシュ計算に使用しています。

このように、特定のプロパティをハッシュ計算から除外することで、必要な情報だけを用いてオブジェクトの一意性を確保できます。

この例を実行すると、Personのインスタンスはidプロパティだけを考慮してハッシュ値を計算することになります。

○サンプルコード12:HashableとEquatableの組み合わせ

Swiftでは、HashableはEquatableプロトコルを継承しているため、==演算子をオーバーロードすることで、2つのオブジェクトの等価性をカスタムに定義することができます。

struct Book: Hashable {
    let title: String
    let author: String

    static func ==(lhs: Book, rhs: Book) -> Bool {
        return lhs.title == rhs.title && lhs.author == rhs.author
    }
}

このコードでは、Book構造体がHashableとEquatableを満たすように実装されています。

具体的には、==演算子をオーバーロードして、titleとauthorの両方が等しい場合に限り、2つのBookオブジェクトを等価とみなすようにしています。

この例を実行すると、2つのBookのインスタンスがtitleとauthorの両方で等しい場合にのみ、等価として扱われることになります。

まとめ

SwiftのHashableプロトコルは、データのユニークな識別や、高速なデータアクセスなど、多岐にわたるタスクの実現に不可欠なツールとなっています。

この記事では、Hashableの基本から応用、さらにカスタマイズ方法に至るまで、徹底的にその活用方法を解説しました。

特に、カスタムHasherの実装や特定のプロパティをハッシュ計算からの除外、さらにはEquatableとの組み合わせなど、より高度な利用シーンに対応するためのテクニックを学ぶことができたかと思います。

これらの知識を駆使することで、Swiftにおけるデータの取り扱いがより柔軟かつ効率的になります。

今回学んだ内容を実際のプログラミングタスクに活かし、Swiftでの開発の幅を広げ、スキルアップを実現しましょう。