キーとそれに紐づいた値をハッシュマップに格納する

一般的なコレクションのトリを飾るのは、ハッシュマップです。型HashMap<K, V>は、 K型のキーとV型の値の対応関係をハッシュ関数を使用して保持します。 ハッシュ関数は、キーと値のメモリ配置方法を決めるものです。多くのプログラミング言語でもこの種のデータ構造はサポートされていますが、 しばしば名前が違います。hash、map、object、ハッシュテーブル、辞書、連想配列など、枚挙に(いとま)はありません。

ハッシュマップは、ベクタのように番号ではなく、どんな型にもなりうるキーを使ってデータを参照したいときに有用です。 例えば、ゲームにおいて、各チームのスコアをハッシュマップで追いかけることができます。ここで、各キーはチーム名、 値が各チームのスコアになります。チーム名が与えられれば、スコアを扱うことができるわけです。

この節でハッシュマップの基礎的なAPIを見ていきますが、より多くのグッズが標準ライブラリにより、 HashMap<K, V>上に定義された関数に隠されています。いつものように、 もっと情報が欲しければ、標準ライブラリのドキュメントをチェックしてください。

新規ハッシュマップを生成する

空のハッシュマップを作成する方法のひとつはnewを使用する方法で、その後要素をinsertで追加することができます。 リスト8-20では、名前がブルーイエローの2チームのスコアを追いかけています。 ブルーチームは10点から、イエローチームは50点から始まります。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

リスト8-20: ハッシュマップを生成してキーと値を挿入する

最初に標準ライブラリのコレクション部分からHashMapuseする必要があることに注意してください。 今までの3つの一般的なコレクションの内、これが最も使用頻度が低いので、初期化処理で自動的にスコープに導入される機能には含まれていません。 また、標準ライブラリからのサポートもハッシュマップは少ないです; 例えば、生成するための組み込みマクロがありません。

ベクタと全く同様に、ハッシュマップはデータをヒープに保持します。このHashMapはキーがString型、 値はi32型です。ベクタのように、ハッシュマップは均質です: すべてのキーは互いに同じ型でなければならず、 値も全て同じ型でなければなりません。

ハッシュマップの値にアクセスする

リスト8-21に示したように、キーをgetメソッドに提供することで、ハッシュマップから値を取り出すことができます。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name).copied().unwrap_or(0);
}

リスト8-21: ハッシュマップに保持されたブルーチームのスコアにアクセスする

ここで、scoreはブルーチームに紐づけられた値になり、結果は10となるでしょう。 getメソッドはOption<&V>を返します; キーに対応する値がハッシュマップになかったら、getNoneを返すでしょう。 このプログラムはこのOptionに対して、Option<&i32>ではなくOption<i32>を得るためにcopiedを呼び出し、 その後、scoresがそのキーに対応するエントリを持たない場合はscoreを0に設定するために、unwrap_orを呼び出して対処します。

ベクタのように、forループでハッシュマップのキーと値のペアを走査することができます:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

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

このコードは、各ペアを任意の順番で出力します:

Yellow: 50
Blue: 10

ハッシュマップと所有権

i32のようなCopyトレイトを実装する型について、値はハッシュマップにコピーされます。 Stringのような所有権のある値なら、値はムーブされ、リスト8-22でデモされているように、 ハッシュマップはそれらの値の所有者になるでしょう。

fn main() {
    use std::collections::HashMap;

    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 and field_value are invalid at this point, try using them and
    // see what compiler error you get!
	// field_nameとfield_valueはこの時点で無効になる。試しに使ってみて
	// どんなコンパイルエラーが出るか確認してみて!
}

リスト8-22: 一旦挿入されたら、キーと値はハッシュマップに所有されることを示す

insertを呼び出してfield_namefield_valueがハッシュマップにムーブされた後は、 これらの変数を使用することは叶いません。

値への参照をハッシュマップに挿入したら、値はハッシュマップにムーブされません。参照が指している値は、 最低でもハッシュマップが有効な間は、有効でなければなりません。これらの問題について詳細には、 第10章の「ライフタイムで参照を有効化する」節で語ります。

ハッシュマップを更新する

キーと値のペアの数は伸長可能ですが、それぞれの一意なキーには同時に1つの値しか紐づけることができません(ただし逆は可能です: 例えば、scoresハッシュマップ内にブルーチームとイエローチームはともに値10を保存することができます)。

ハッシュマップ内のデータを変えたい時は、すでにキーに値が紐づいている場合の扱い方を決めなければなりません。 古い値を新しい値で置き換えて、古い値を完全に無視することもできます。古い値を保持して、 新しい値を無視し、キーにまだ値がない場合に新しい値を追加するだけにすることもできます。 あるいは、古い値と新しい値を組み合わせることもできます。各方法について見ていきましょう!

値を上書きする

キーと値をハッシュマップに挿入し、同じキーを異なる値で挿入したら、そのキーに紐づけられている値は置換されます。 リスト8-23のコードは、insertを二度呼んでいるものの、ハッシュマップには一つのキーと値の組しか含まれません。 なぜなら、ブルーチームキーに対する値を2回とも挿入しているからです。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

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

リスト8-23: 特定のキーで保持された値を置き換える

このコードは、{"Blue": 25}と出力するでしょう。10という元の値は上書きされたのです。

キーが存在しない場合のみキーと値を追加する

特定のキーがハッシュマップ内に値とともに存在しているか確認して、以下のアクションを取ることは一般的でしょう: キーがハッシュマップに存在する場合は、既存の値はそのままにします。 キーが存在しない場合は、そのキーとそれに対応する値を挿入します。

ハッシュマップには、これを行うentryと呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。 このentryメソッドの戻り値は、Entryと呼ばれるenumであり、これは存在したりしなかったりする可能性のある値を表します。 イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、 50という値を挿入したく、ブルーチームに対しても同様です。entryAPIを使用すれば、コードはリスト8-24のようになります。

fn main() {
    use std::collections::HashMap;

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

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

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

リスト8-24: entryメソッドを使ってキーに値がない場合だけ挿入する

Entry上のor_insertメソッドは、対応するEntryキーが存在した時にそのキーに対する値への可変参照を返すために定義されており、 もしなかったら、引数をこのキーの新しい値として挿入し、新しい値への可変参照を返します。このテクニックの方が、 そのロジックを自分で書くよりもはるかに綺麗な上に、borrow checkerとも親和性が高くなります。

リスト8-24のコードを実行すると、{"Yellow": 50, "Blue": 10}と出力するでしょう。 最初のentry呼び出しは、まだイエローチームに対する値がないので、値50でイエローチームのキーを挿入します。 entryの2回目の呼び出しはハッシュマップを変更しません。なぜなら、ブルーチームには既に10という値があるからです。

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

ハッシュマップの別の一般的なユースケースは、キーの値を探し、古い値に基づいてそれを更新することです。 例えば、リスト8-25は、各単語があるテキストに何回出現するかを数え上げるコードを示しています。 キーに単語を入れたハッシュマップを使用し、その単語を何回見かけたか追いかけるために値を増やします。 ある単語を見かけたのが最初だったら、まず0という値を挿入します。

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
}

リスト8-25: 単語とカウントを保持するハッシュマップを使って単語の出現数をカウントする

このコードは、{"world": 2, "hello": 1, "wonderful": 1}と出力するでしょう。 同じキー/値ペアが、異なる順で印字されるかもしれません: 「ハッシュマップの値にアクセスする」の節で説明した、 ハッシュマップの走査は任意の順で起こるということを思い出してください。

split_whitespaceメソッドは、textの値から、ホワイトスペースによって区切られた部分スライスを走査するイテレータを返します。 or_insert関数は、指定されたキーに対する値への可変参照(&mut V)を返すのです。 ここでその可変参照をcount変数に保持しているので、その値に代入するには、 まずアスタリスク(*)でcountを参照外ししなければならないのです。この可変参照は、 forループの終端でスコープを抜けるので、これらの変更は全て安全であり、借用規則により許可されるのです。

ハッシュ関数

HashMapはデフォルトでは、ハッシュテーブルに関するサービス拒否(DoS)攻撃に対する耐性を提供する、SipHashと呼ばれるハッシュ関数を使用します1。 これは、利用可能な最速のハッシュアルゴリズムではありませんが、パフォーマンスの欠落と引き換えに安全性を得るというトレードオフは、 価値があります。自分のコードをプロファイリングして、自分の目的では標準のハッシュ関数は遅すぎると判明したら、 異なるhasherを指定することで別の関数に切り替えることができます。hasherとは、 BuildHasherトレイトを実装する型のことです。トレイトについてとその実装方法については、第10章で語ります。 必ずしも独自のhasherを1から作り上げる必要はありません; crates.ioには、 他のRustユーザによって共有された多くの一般的なハッシュアルゴリズムを実装したhasherを提供するライブラリがあります。

まとめ

ベクタ、文字列、ハッシュマップはデータを保持し、アクセスし、変更する必要のあるプログラムで必要になる、 多くの機能を提供してくれるでしょう。今なら解決可能なはずの練習問題を用意しました:

  • 整数のリストが与えられ、ベクタを使ってmedian(ソートされた時に真ん中に来る値)、 mode(最も頻繁に出現する値; ハッシュマップがここでは有効活用できるでしょう)を返してください。
  • 文字列をピッグ・ラテン(訳注: 英語の言葉遊びの一つ)に変換してください。各単語の最初の子音は、 単語の終端に移り、"ay"が足されます。従って、"first"は"irst-fay"になります。ただし、 母音で始まる単語には、お尻に"hay"が付け足されます("apple"は"apple-hay"になります)。 UTF-8エンコードに関する詳細を心に留めておいてください!
  • ハッシュマップとベクタを使用して、ユーザに会社の部署に雇用者の名前を追加させられるテキストインターフェイスを作ってください。 例えば、"Add Sally to Engineering"(開発部門にサリーを追加)や"Add Amir to Sales"(販売部門にアミールを追加)などです。 それからユーザに、ある部署にいる人間の一覧や部署ごとにアルファベット順で並べ替えられた会社の全人間の一覧を扱わせてあげてください。

標準ライブラリのAPIドキュメントには、この練習問題に有用な、ベクタ、文字列、ハッシュマップのメソッドが解説されています。

処理が失敗することもあるような、より複雑なプログラムに入り込んできています。 ということは、エラーの処理法について議論するのにぴったりということです。次はそれをします!