Rustプログラミングにおいて、データを効率的に管理するためのコレクション型を理解することは非常に重要です。

特に、特定のキーに関連付けられた値を高速に検索できる「HashMap」は、実用的なアプリケーション開発で最も頻繁に使用されるデータ構造の一つです。

RustのHashMapは、標準ライブラリの std::collections モジュールに用意されており、安全性とパフォーマンスを両立させた設計がなされています。

この記事では、HashMapの基本的な使い方から、所有権の概念、実戦的なイテレーション、さらにはパフォーマンス最適化のヒントまでを詳しく解説します。

これからRustを学ぶ方はもちろん、より効率的なコードを書きたい中級者の方にとっても役立つ内容を網羅しました。

HashMapの基本概念と導入

HashMapは、キー(Key)と値(Value)のペアを格納するデータ構造であり、一般的に「ハッシュマップ」や「連想配列」と呼ばれます。

RustのHashMapは、ハッシュ関数を利用してデータを格納する場所を決定するため、データの追加や検索を非常に高速に行うことができます。

HashMapを使用する際は、まず標準ライブラリからインポートを行う必要があります。

ベクタ(Vec)や文字列(String)とは異なり、HashMapはプレリュード(自動的にインポートされるモジュール)に含まれていないため、明示的な宣言が必要です。

Rust
// HashMapを使用するためにインポートが必要
use std::collections::HashMap;

fn main() {
    // 新しい空のHashMapを作成する
    let mut scores = HashMap::new();

    // データの挿入
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    println!("{:?}", scores);
}
実行結果
{"Blue": 10, "Yellow": 50}

上記のコードでは、HashMap::new() を使用して新しいインスタンスを生成しています。

RustのHashMapは型推論が働くため、最初に挿入されたデータに基づいて型が決まります。

この例では、キーが String 型、値が i32 型として推論されています。

基本的な操作:挿入・取得・削除

HashMapを使いこなすためには、データの基本的な操作方法を習得する必要があります。

ここでは、値の挿入(insert)、取得(get)、更新、そして削除(remove)について解説します。

データの挿入と上書き

insert メソッドを使用すると、新しいペアを追加できます。

もし既に同じキーが存在している場合、古い値は新しい値に上書きされます。

データの取得

値を取り出すときは、get メソッドを使用します。

get メソッドは、指定したキーが存在しない可能性を考慮して Option<&V> を返します。

Rust
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    let team_name = String::from("Blue");
    
    // getはOption型を返す
    match scores.get(&team_name) {
        Some(score) => println!("スコアは {} です", score),
        None => println!("チームが見つかりません"),
    }
}
実行結果
スコアは 10 です

get メソッドが返すのは値そのものではなく、値への参照であることに注意してください。

データの削除

特定のキーを持つデータを削除したい場合は、remove メソッドを利用します。

このメソッドは、削除された値を Option<V> として返すため、削除したデータをそのまま別の処理に使うことも可能です。

HashMapと所有権の仕組み

RustにおけるHashMapの動作を理解する上で、所有権(Ownership)のルールを無視することはできません。

HashMapにデータを挿入すると、そのデータの所有権がどう移動するのかを確認しておきましょう。

i32 のような Copy トレイトを実装している型の場合、値はHashMapにコピーされます。

一方で、String のような Copy トレイトを実装していない型の場合、所有権はHashMapへと移動(Move)します。

Rust
use std::collections::HashMap;

fn main() {
    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);

    // ここで field_name や field_value を使うとコンパイルエラーになる
    // println!("{}", field_name); // 所有権が移動しているため使用不可
}

もし値をHashMapに入れた後も元の変数を使用したい場合は、参照を格納するか、clone メソッドを使用して複製を作る必要があります。

ただし、参照を格納する場合はライフタイムの指定が必要になるため、初心者のうちは所有権を移動させる設計がシンプルで推奨されます。

Entry APIを活用した効率的な更新

RustのHashMapには、値の存在確認と更新を同時に行う「Entry API」という便利な仕組みがあります。

「もし値がなければ挿入し、あれば何もしない」といった条件付きの操作を、簡潔に記述できるのが特徴です。

or_insertメソッドの使い方

entry メソッドは Entry という列挙型を返し、それに対して or_insert を呼び出すことで初期値を設定できます。

Rust
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    // "Yellow" というキーがなければ 50 を挿入する
    scores.entry(String::from("Yellow")).or_insert(50);
    
    // "Blue" は既に存在するので何もしない
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
}
実行結果
{"Yellow": 50, "Blue": 10}

古い値に基づいて更新する

Entry APIの強力な点は、既存の値をインプレースで更新できる点にあります。

例えば、文章内の単語の出現回数をカウントする処理は、以下のように非常にスマートに記述できます。

Rust
use std::collections::HashMap;

fn main() {
    let text = "hello world wonderful world";
    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        // キーが存在すればその値の可変参照を返し、なければ 0 を挿入してから参照を返す
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
}
実行結果
{"hello": 1, "world": 2, "wonderful": 1}

or_insert メソッドは、値への可変参照(&mut V)を返すため、デリファレンス(*)を行って直接書き換えることが可能です。

HashMapのイテレーション

HashMapに格納されたすべての要素に対して処理を行うには、for ループを使用します。

イテレーションを行う際、キーと値のペアは任意の順序で取り出されることに注意してください。

Rust
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    scores.insert(String::from("Green"), 30);

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

HashMapは内部的にハッシュテーブルを使用しているため、要素が追加された順序は保持されません。

もし順序を保持したい場合は、外部クレートの indexmap などを検討する必要があります。

HashMapのパフォーマンスとハッシュ関数

Rustの標準HashMapは、デフォルトで SipHash というハッシュアルゴリズムを採用しています。

このアルゴリズムは、ハッシュ衝突を利用したDoS攻撃(HashDoS)に対して高い耐性を持っています。

セキュリティ面では非常に優秀ですが、非常に小さなキーを大量に処理する場合など、パフォーマンスが最優先されるケースではオーバーヘッドが気になることもあります。

カスタムハッシャーの使用

速度を極限まで追求したい場合、デフォルトのハッシャーを別のものに変更することが可能です。

例えば、fxhash クレートなどが提供する高速なハッシュアルゴリズムを導入することで、パフォーマンスを改善できる場合があります。

ただし、これらは標準のハッシャーが提供するセキュリティ機能をトレードオフにしているため、用途に応じて慎重に選択してください。

初期容量の指定

HashMapの要素数が事前にある程度わかっている場合は、with_capacity メソッドを使用するのが効率的です。

デフォルトの new で作成すると、要素が増えるたびにメモリの再割り当て(Reallocation)が発生し、パフォーマンスの低下を招きます。

Rust
// 100個の要素が入ることを見越してメモリを確保する
let mut map = HashMap::with_capacity(100);

HashMapの主なメソッド一覧

利用頻度の高いメソッドを以下の表にまとめました。

メソッド名説明戻り値の型
insert(k, v)キーと値を挿入する。既存なら上書き。Option<V>
get(&k)指定したキーに対応する値の参照を取得。Option<&V>
remove(&k)指定したキーのペアを削除する。Option<V>
contains_key(&k)キーが存在するかどうかを判定する。bool
entry(k)更新や挿入のためのEntry APIを返す。Entry
len()格納されている要素数を返す。usize
is_empty()マップが空かどうかを判定する。bool

よくあるエラーと注意点

HashMapを使用する際に初心者が陥りやすいポイントがいくつかあります。

まず一つ目は、キーとして使用できる型の制限です。

HashMapのキーにする型は、Eq トレイトと Hash トレイトを実装していなければなりません。

自作の構造体をキーにする場合は、#[derive(PartialEq, Eq, Hash)] を付与してこれらのトレイトを自動実装させる必要があります。

二つ目は、浮動小数点数(f32, f64)をキーにできない点です。

浮動小数点数は NaN (Not a Number) の存在により、厳密な等価比較が定義できないため、デフォルトでは Eq トレイトを実装していません。

数値をキーにする場合は整数型を使用するか、特別なラッパー型を検討してください。

まとめ

RustのHashMapは、柔軟かつ強力なデータ構造であり、適切に使うことでプログラムの効率を大幅に向上させることができます。

基本操作である insertget だけでなく、Rust特有の Entry API をマスターすることで、冗長な条件分岐を省いた洗練されたコードを書けるようになります。

また、所有権のルールやハッシュ関数の特性を理解しておくことは、予期せぬコンパイルエラーやパフォーマンス不足を防ぐ鍵となります。

本記事で紹介した基礎知識と実践的なテクニックを、ぜひあなたのRustプロジェクトに活用してみてください。

まずは小さなツール作成から HashMap を取り入れ、その便利さを体感してみることをおすすめします。