Rust入門

【Rust入門】HashMap 型の基本について分かりやすく解説

【Rust入門】HashMap 型の基本について分かりやすく解説
naoki-hn

Rust でデータをキーと値のペアで管理するための HashMapの基本を初心者にも分かりやすく解説します。

HashMap<K, V>

Rust には、同じ型のデータをまとめて管理するための「コレクション(Collections)」が用意されています。コレクションは、実行中に要素数を変更できる柔軟なデータ構造です。

この記事では、コレクション型の代表例である HashMap<K, V> 型について解説します。

HashMap<K, V> 型とは

HashMap<K, V>は、キー(Key)と値(Value)のペアでデータを管理するためのコレクション型です。

Vec<T> 型がインデックスで要素にアクセスするのに対して、HashMap はキーを使って値を取得します。コレクション型は、内部でヒープ領域を利用してデータを管理するため、プログラム実行時のデータ長の変更や大きなサイズのデータの扱いに適します。

例えば、「ユーザー ID からユーザー名を取得する」「商品 ID から価格を取得する」というような場合を扱う際のデータ管理に非常に適しています。

その他の代表的なコレクションである Vec については、以下を参考にしてください。

HashMap の生成方法

HashMap を使う準備

HashMap は標準ライブラリの std::collections モジュールで提供されています。利用するには以下のようにインポートします。

use std::collections::HashMap;

以降で紹介する各プログラムでも先頭でインポートをしています。

関連関数 HashMap::new() を使用する

HashMap 型を生成する最も基本的な方法は、new メソッドを使用する方法です。キーや値を追加するには、insert メソッドを使用します。

use std::collections::HashMap;

fn main() {
    // HashMap を生成する
    let mut scores = HashMap::new();

    // キー、値を追加する
    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);

    println!("{:?}", scores);
}
【実行結果】
{"Alice": 80, "Bob": 100}

なお、HashMap は要素の順序を保証しません。そのため、実行により表示順序が異なる場合があります。

collect メソッドを使用する

HashMap は、イテレータから collect メソッドを使用して生成することも可能です。

use std::collections::HashMap;

fn main() {
    let keys = vec![String::from("Alice"), String::from("Bob")];
    let values = vec![80, 10];

    let scores: HashMap<String, i32> = keys.into_iter().zip(values.into_iter()).collect();

    println!("{:?}", scores);
}
【実行結果】
{"Alice": 80, "Bob": 10}

上記例では、keysvalues という Vec 型の変数を用意し、zip によりペアを生成して collect により HashMap 型に集約しています。collect メソッドは任意のコレクションにまとめることができるため、型注釈をつけるようにしてください。

collect については、イテレータの消費メソッドに関する以下記事で紹介しているので参考にしてください。

Iteratorの消費アダプタ(sum・fold・collect など)を分かりやすく解説

HashMap 要素の取得・追加・更新・削除

要素の取得

HashMap の要素値を取得するには、get メソッドによりキーを指定します。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    // Alice のスコアを追加
    scores.insert(String::from("Alice"), 80);

    // Alice のスコアを取得
    let alice_score = scores.get("Alice");
    match alice_score {
        Some(score) => println!("Alice のスコア : {}", score),
        None => println!("Alice のスコアが見つかりませんでした。"),
    }

    // Bob のスコアを取得(存在しないキー)
    let bob_score = scores.get("Bob");
    match bob_score {
        Some(score) => println!("Bob のスコア : {}", score),
        None => println!("Bob のスコアが見つかりませんでした。"),
    }
}
【実行結果】
Alice のスコア : 80
Bob のスコアが見つかりませんでした。

get は、Option<&V> 型 を返却するため、match を使って値の有無を確認して安全に処理することが可能です。キーが存在しない場合には、None となります。

要素の追加・更新

新しい要素の追加や更新には、insert メソッドを使用します。例のように、同じキーに対して insert を実行すると更新となる点に注意してください。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    println!("初期状態 : {:?}", scores);

    // スコアを追加する
    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);

    println!("追加後 : {:?}", scores);

    // Alice のスコアを更新する(上書き)
    scores.insert(String::from("Alice"), 95);

    println!("更新後 : {:?}", scores);
}
【実行結果】
初期状態 : {}
追加後 : {"Bob": 100, "Alice": 80}
更新後 : {"Bob": 100, "Alice": 95}

なお、String のような所有権を持つ型の場合、変数を使用している場合は、insert 時に所有権が移動し、元の変数は使えなくなるので注意しましょう。

let name = String::from("Alice");

scores.insert(name, 80);

// 所有権が移動するためエラーとなる。
// println!("{name}");

要素の削除

要素を削除する場合には、remove メソッドを使用します。存在しないキーを削除しても何も起こりません。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);
    scores.insert(String::from("Charlie"), 70);

    println!("削除前 : {:?}", scores);

    // キーを指定して削除する
    scores.remove("Alice");

    println!("Alice 削除後 : {:?}", scores);

    // 存在しないキーを削除しても何も起きない
    scores.remove("Dave");

    println!("Dave 削除後(変化なし) : {:?}", scores);
}
【実行結果】
削除前 : {"Bob": 100, "Charlie": 70, "Alice": 80}
Alice 削除後 : {"Bob": 100, "Charlie": 70}
Dave 削除後(変化なし) : {"Bob": 100, "Charlie": 70}

存在しない場合のみ値を追加する場合(Entry API)

HashMap にキーが存在しない場合にのみ値を追加したい場合は、Entry API を使用するのが便利です。entry により対象を取得し、or_insert で値を追加することで、存在しない場合にのみ値を追加することができます。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert(String::from("Alice"), 80);

    println!("初期状態 : {:?}", scores);

    // Alice はすでに存在するので更新されない
    scores.entry(String::from("Alice")).or_insert(0);

    // Bob は存在しないので追加される
    scores.entry(String::from("Bob")).or_insert(100);

    println!("or_insert 後 : {:?}", scores);
}
【実行結果】
初期状態 : {"Alice": 80}
or_insert 後 : {"Bob": 100, "Alice": 80}

繰り返し処理(for)での利用方法

HashMap は複数の要素を管理するため、for を利用した繰り返し処理を行うことがよくあります。Rust の繰り返しでは、イテレータを取得して動作します。

参照のみの繰り返し処理

HashMap の要素を 1 つずつ取り出して参照するだけの繰り返しの場合には、以下のように不変参照 (&) を使って for を使用します。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);
    scores.insert(String::from("Charlie"), 70);

    // 所有権を移動せず参照としてループする
    for (name, score) in &scores {
        println!("{name} のスコア : {score}");
    }

    // ループ後も scores を使用できる
    println!("ループ後のエントリ数 : {}", scores.len());
}
【実行結果】
Bob のスコア : 100
Alice のスコア : 80
Charlie のスコア : 70
ループ後のエントリ数 : 3

キーと値は、(name, score) にそれぞれ 1 要素ずつ取り出して繰り返し処理が進みます。

値の変更を伴う繰り返し処理

HashMap の要素を繰り返しの中で変更する場合には、以下のように可変参照 (&mut) を使って for を使用します。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);
    scores.insert(String::from("Charlie"), 70);

    println!("更新前 : {:?}", scores);

    // 可変参照でループし、全員のスコアに 10 を加算する
    for (_name, score) in &mut scores {
        *score += 10;
    }

    println!("更新後 : {:?}", scores);
}
【実行結果】
更新前 : {"Alice": 80, "Bob": 100, "Charlie": 70}
更新後 : {"Alice": 90, "Bob": 110, "Charlie": 80}

値を変更する場合には、*score のように参照を外して変更することが可能です。

所有権の移動を伴う繰り返し処理

HashMap の所有権を移動して繰り返し処理をする場合には、以下のように in の部分に HashMap の変数を直接指定します。

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();

    scores.insert(String::from("Alice"), 80);
    scores.insert(String::from("Bob"), 100);
    scores.insert(String::from("Charlie"), 70);

    // 所有権を移動させてループする
    for (name, score) in scores {
        println!("{name} のスコア : {score}");
    }

    // 所有権が移動したため、ループ後は scores を使用できない
    // println!("{:?}", scores); // コンパイルエラー
}
【実行結果】
Charlie のスコア : 70
Bob のスコア : 100
Alice のスコア : 80

所有権を移動する繰り返し処理をした場合には、繰り返し後には変数を使うことができなくなります。この方法は、ループ後に元の HashMap を使わない場合や大きいサイズの HashMap のためループ終了にあわせてメモリ解放したい場合などに利用できます。

まとめ

Rust でデータをキーと値のペアで管理するための HashMapの基本を解説しました。

HashMap<K, V> 型は、キーと値のペアでデータを管理するためのコレクション型です。ユーザー ID とユーザー名、商品 ID と価格など、対応関係を持つデータを効率的に管理できます。

この記事では、HashMap の生成方法から要素の取得・追加・更新・削除、Entry API を利用した値の追加方法、for を使った繰り返し処理まで紹介しました。

HashMap は Rust のプログラムで非常によく利用されるコレクション型です。Vec 型とあわせて使う機会も多いため、この記事で紹介した基本操作をしっかり身につけておきましょう。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました