HashMapでデータをカウントする

RustBeginner
オンラインで実践に進む

はじめに

ベクターは、数値の位置によって値を整理します。一方、HashMap<K, V> は、型 K の各一意なキーに型 V の値を1つ関連付けます。そのため、カウントや設定など、キーで管理するデータに適しています。

この実験では、小さな単語数マップを作成します。その後、カウントの取得と置換、既存の単語と存在しない単語に対するentry APIの使用、マップで未指定のキー順序に依存しないすべての値の反復処理を行います。

キーに対応する値を挿入・取得する

このステップでは、2組のキーと値を挿入し、1つのカウントを安全に取得します。

プロジェクトに移動して、ソースファイルを開きます。

cd /home/labex/project/word-counts
nano src/main.rs

用意されているパス std::collections::HashMap は、Rust標準ライブラリのコレクションモジュールにある HashMap を指定しています。use 行によって、この長いパスにローカルで使える短い名前 HashMap が与えられます。ここではこれを準備済みのインポート処理として扱います。パスとインポートについては、後のモジュールLabで詳しく学びます。HashMap<String, u32> は、所有権を持つ文字列キーと符号なし整数のカウントを格納します。Step 1aのコメントを次のコードに置き換えます。

    let _ = counts.insert(String::from("rust"), 1);
    let _ = counts.insert(String::from("safe"), 1);

insert は、そのキーに以前格納されていた値があれば返します。ここで使うキーは新しいため、結果は None になります。let _ = ...; というパターンは、処理を実行したうえで、その結果を意図的に破棄します。この単独の _ は、_returned_id のような名前とは異なります。単独のワイルドカードは、後から利用できる束縛を作りません。

Step 1bのコメントを次のコードに置き換えます。

    match counts.get("rust") {
        Some(count) => println!("Rust count: {count}"),
        None => println!("Rust count: missing"),
    }

get は借用したテキスト "rust" を受け取り、キーやカウントをマップから移動させずに Option<&u32> を返します。保存して終了したら、確認して実行します。

cargo check
cargo run --quiet
Unique words: 2
Rust count: 1

1行目はキーが一意であることを示し、2行目は保存されたカウントを検索できたことを示します。

既存のカウントを置き換える

このステップでは、既存のキーに対して insert を使い、置き換えられた値を確認します。

ソースファイルを開きます。

nano src/main.rs

Step 2のコメントを次のコードに置き換えます。

    let previous = counts.insert(String::from("rust"), 2);
    println!("Previous rust count: {previous:?}");

マップは、1つのキーにつき1つの値だけを保持します。新しい値 2 は 1 を置き換え、返された Some(1) によって、その置換を確認できます。保存して終了したら、実行します。

cargo run --quiet

追加された行は次のようになります。

Previous rust count: Some(1)

置換によって別のキーが作成されることはないため、単語数は2のままです。

Entry APIでカウントを更新する

このステップでは、同じAPIを使って、既存の単語と存在しない単語のカウントを1つずつ増やします。

entry(key).or_insert(default) は、キーに対応する既存の値への可変参照を返します。値が存在しない場合はデフォルト値を挿入し、その値への可変参照を返します。ソースファイルを開きます。

nano src/main.rs

Step 3のコメントを次のコードに置き換えます。

    *counts.entry(String::from("safe")).or_insert(0) += 1;
    *counts.entry(String::from("fast")).or_insert(0) += 1;
    println!("Safe count: {}", counts["safe"]);
    println!("Fast count: {}", counts["fast"]);

この簡潔な更新処理を内側から順に読みます。まず、entry(key) は、そのキーに対応する場所を選択します。そこに値が存在する場合でも、存在しない場合でも同じように動作します。次に、or_insert(0) が値の存在を保証し、その値への可変参照 &mut u32 を返します。

先頭の * は、その参照が指している u32 にアクセスします。最後に、+= 1 が1を加算し、新しいカウントをマップに保存します。

そのため、safe は 2 になります。一方、存在しない fast は0から始まり、1 になります。角括弧を使うと、キーで値を読み取れます。キーが存在しない場合、マップのインデックス操作はパニックになります。ここで安全に使えるのは、直前の2つの entry 呼び出しによって両方のキーの存在が保証されているためです。キーが存在しない可能性がある場合は、引き続き get を使ってください。

保存して終了したら、実行します。

cargo run --quiet

最後の行は次のようになります。

Safe count: 2
Fast count: 1

1つの操作で、値が存在するキーと存在しないキーの両方を処理できました。

キーの順序に依存せずに反復処理する

このステップでは、保存されているすべてのカウントを反復処理し、順序に依存しない合計を計算します。

ハッシュマップは、キーの反復順序が一定であることを保証しません。そのため、初学者向けの確認では、キーが特定の順序で表示されることを前提にしないでください。values() メソッドは、保存されているすべての値を処理するため、合計を計算できます。

ソースファイルを開き、Step 4のコメントを置き換えます。

nano src/main.rs
    let mut total = 0;
    for count in counts.values() {
        total += count;
    }
    println!("Total occurrences: {total}");

ここで count は共有参照です。+= を使うと、参照先の整数を加算できます。保存して終了したら、確認して実行します。

cargo check
cargo run --quiet

最後の行は次のようになります。

Total occurrences: 5

この合計は、rust = 2、safe = 2、fast = 1 を、これらの値の走査順序に依存せずに合算したものです。

まとめ

キーに対応するカウントを挿入、取得、置換、増加させた後、キーの順序が一定であることを前提にせず、マップ内のすべての値を反復処理しました。