はじめに
ハッシュマップでキーと関連する値を格納するへようこそ。この実験(Lab)はRust ブックの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、ハッシュマップの概念と、キーと関連する値を格納するためにどのように使用できるかを探ります。
ハッシュマップでキーと関連する値を格納する
一般的なコレクションの最後は、ハッシュマップ です。型 HashMap<K, V> は、ハッシュ関数 を使用して型 K のキーと型 V の値のマッピングを格納します。このハッシュ関数は、これらのキーと値をメモリ内にどのように配置するかを決定します。多くのプログラミング言語ではこの種のデータ構造がサポートされていますが、しばしば異なる名前で呼ばれます。例えば、_ハッシュ_、_マップ_、_オブジェクト_、_ハッシュテーブル_、_辞書_、または 連想配列 などがあります。
ハッシュマップは、ベクターのようにインデックスを使ってではなく、任意の型のキーを使ってデータを検索したい場合に便利です。たとえば、ゲームでは、各チームの名前をキーとし、各チームの得点を値とするハッシュマップで各チームの得点を管理することができます。チーム名が与えられれば、その得点を取得することができます。
このセクションではハッシュマップの基本的な API について説明しますが、標準ライブラリで HashMap<K, V> に定義されている関数には他にもたくさんの便利な機能があります。いつものように、詳細については標準ライブラリのドキュメントを参照してください。
新しいハッシュマップを作成する
空のハッシュマップを作成する方法の 1 つは、new を使用して insert で要素を追加することです。リスト 8-20 では、チーム名が Blue と Yellow の 2 つのチームの得点を管理しています。Blue チームは 10 点から始め、Yellow チームは 50 点から始めます。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
リスト 8-20: 新しいハッシュマップを作成し、いくつかのキーと値を挿入する
標準ライブラリのコレクション部分から HashMap を最初に use する必要があることに注意してください。3 つの一般的なコレクションの中で、これは最も使用頻度が低いため、プレリュードで自動的にスコープに持ち込まれる機能には含まれていません。ハッシュマップは標準ライブラリからのサポートも少なく、たとえば組み込みのマクロで構築することはできません。
ベクターと同じように、ハッシュマップはデータをヒープに格納します。この HashMap は String 型のキーと i32 型の値を持っています。ベクターと同様に、ハッシュマップは同質です。すべてのキーは同じ型でなければならず、すべての値も同じ型でなければなりません。
ハッシュマップ内の値にアクセスする
リスト 8-21 に示すように、get メソッドにキーを指定することで、ハッシュマップから値を取得することができます。
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: ハッシュマップに格納された Blue チームの得点にアクセスする
ここで、score は Blue チームに関連付けられた値を持ち、結果は 10 になります。get メソッドは Option<&V> を返します。ハッシュマップにそのキーに対応する値がない場合、get は None を返します。このプログラムでは、copied を呼び出して Option<&i32> ではなく Option<i32> を取得し、その後 unwrap_or を使用して、scores にキーのエントリがない場合に score をゼロに設定することで、Option を処理しています。
ベクターと同様に、for ループを使用してハッシュマップ内の各キー - 値のペアを反復処理することができます。
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 で示されています。
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!
リスト 8-22: キーと値が挿入されると、ハッシュマップがそれらを所有することを示す
insert を呼び出して field_name と field_value がハッシュマップにムーブされた後は、これらの変数を使用することはできません。
値の参照をハッシュマップに挿入した場合、値はハッシュマップにムーブされません。参照が指す値は、少なくともハッシュマップが有効である間は有効でなければなりません。これらの問題については、「ライフタイムで参照を検証する」で詳しく説明します。
ハッシュマップを更新する
キーと値のペアの数は増やすことができますが、それぞれの一意のキーには一度に 1 つの値しか関連付けることができません(ただし、逆は成り立ちません。たとえば、Blue チームと Yellow チームの両方が scores ハッシュマップに値 10 を格納することができます)。
ハッシュマップ内のデータを変更したい場合、キーにすでに値が割り当てられている場合の処理方法を決める必要があります。古い値を新しい値で置き換え、古い値を完全に無視することができます。古い値を保持して新しい値を無視し、キーにまだ値が割り当てられていない場合にのみ新しい値を追加することもできます。または、古い値と新しい値を組み合わせることもできます。それでは、これらのそれぞれの方法を見てみましょう!
値を上書きする
ハッシュマップにキーと値を挿入し、その後同じキーに異なる値を挿入すると、そのキーに関連付けられた値は置き換えられます。リスト 8-23 のコードでは insert を 2 回呼び出していますが、2 回とも Blue チームのキーに対する値を挿入しているため、ハッシュマップには 1 つのキー - 値のペアしか含まれません。
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 は上書きされています。
キーが存在しない場合のみキーと値を追加する
特定のキーがハッシュマップにすでに値と関連付けられているかどうかを確認し、以下のアクションを実行することはよくあることです。キーがハッシュマップに存在する場合、既存の値はそのままにします。キーが存在しない場合、キーとそれに対する値を挿入します。
ハッシュマップにはこのための特別な API があり、entry と呼ばれています。この API は、確認したいキーをパラメータとして受け取ります。entry メソッドの戻り値は Entry という列挙型で、存在する場合と存在しない場合がある値を表します。Yellow チームのキーに関連付けられた値があるかどうかを確認したいとしましょう。値がない場合、値 50 を挿入したいとします。Blue チームについても同様です。entry API を使用すると、コードはリスト 8-24 のようになります。
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 キーが存在する場合、そのキーの値への可変参照を返し、存在しない場合、パラメータをこのキーの新しい値として挿入し、新しい値への可変参照を返すように定義されています。この手法は、自分でロジックを書くよりもはるかにクリーンであり、さらに、借用チェッカーとの相性も良いです。
リスト 8-24 のコードを実行すると、{"Yellow": 50, "Blue": 10} が出力されます。最初の entry 呼び出しでは、Yellow チームにまだ値がないため、Yellow チームのキーに値 50 が挿入されます。2 回目の entry 呼び出しでは、Blue チームにすでに値 10 があるため、ハッシュマップは変更されません。
古い値に基づいて値を更新する
ハッシュマップのもう 1 つの一般的な使用例は、キーの値を検索し、古い値に基づいてそれを更新することです。たとえば、リスト 8-25 は、あるテキスト内の各単語が何回出現するかをカウントするコードを示しています。単語をキーとするハッシュマップを使用し、その単語を何回見たかを追跡するために値をインクリメントします。単語を初めて見る場合は、まず値 0 を挿入します。
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 は SipHash と呼ばれるハッシュ関数を使用しています。この関数は、ハッシュテーブルを利用したサービス拒否 (DoS) 攻撃に対する耐性を提供します。これは利用可能な中で最速のハッシュアルゴリズムではありませんが、パフォーマンスの低下と引き換えに得られるセキュリティの向上は価値があります。コードのプロファイリングを行い、デフォルトのハッシュ関数が目的に対して遅すぎることがわかった場合は、別のハッシャーを指定することで別の関数に切り替えることができます。ハッシャー は BuildHasher トレイトを実装した型です。トレイトとその実装方法については第 10 章で説明します。必ずしも独自のハッシャーを最初から実装する必要はありません。https://crates.io には、他の Rust ユーザーが共有している、多くの一般的なハッシュアルゴリズムを実装したハッシャーを提供するライブラリがあります。
まとめ
おめでとうございます!「ハッシュマップで関連付けられた値を持つキーを格納する」実験を完了しました。LabEx でさらに多くの実験を行い、スキルを向上させることができます。