Rust の HashMap の entry とは
「Rust entry」などで検索すると HashMap の entry を説明してくださっている記事は多く出てくるので、ここでは簡単な説明にとどめます。
参考: RustのHashMapはentryが便利
参考: Rustでdefaultdict
マップや辞書などのデータ構造を扱う際、「あるキーの値が、あればなんらかの処理、なければ新たにデータを追加してから処理」という操作をしたいことは多いと思います。
たとえば、「キーの出現回数をマップに保存する」というケースを考えます。愚直に実装をすると、条件分岐を用いて、「マップにキーがあるか判定し、あれば処理、なければ追加」という操作を記述するかと思います。
let mut map = std::collections::HashMap::new();
let key = "one";
// map.insert(key, 3); // コメントアウトを外すと出力が{"one": 4}になる
if let Some(one) = map.get_mut(key) {
*one += 1;
} else {
map.insert(key, 1);
}
println!("{:?}", map); // {"one": 1}
しかし、この書き方はマップアクセスを2度記述しているので、冗長です。
そこで、HashMap::entry を用いることで、同等の処理を1行で容易に記述できます。
let mut map = std::collections::HashMap::new();
let key = "one";
// map.insert(key, 3); // コメントアウトを外すと出力が{"one": 4}になる
*map.entry(key).or_insert(0) += 1;
println!("{:?}", map); // {"one": 1}
ユースケース別書き方
こちらが今回のメインです。ユースケースから hash_map::Entry 以下に生えている各メソッドをまとめていきたいと思います。
といいつつユースケースが2つほどしか思いついていません…
数値のマップで出現回数を数える場合
初めに例として挙げたものです。key ごとに何回出現したかなどを記録することができます。
or_insert()
を用いて次のように書くことができます。
let mut map = std::collections::HashMap::new();
for c in "abcabc".chars() {
*map.entry(c).or_insert(0) += 1;
}
println!("{:?}", map); // {'c': 2, 'a': 2, 'b': 2}
また、型が決まっていれば、 or_default()
を用いることもできます。
let mut map = std::collections::HashMap::<_, usize>::new();
for c in "abcabc".chars() {
*map.entry(c).or_default() += 1;
}
println!("{:?}", map); // {'c': 2, 'a': 2, 'b': 2}
あるいは、and_modify()
を用いるのもいいのかもしれません。
let mut map = std::collections::HashMap::new();
for c in "abcabc".chars() {
map.entry(c).and_modify(|e| *e += 1).or_insert(1);
}
println!("{:?}", map); // {'c': 2, 'a': 2, 'b': 2}
リストのマップで保存していく場合
これも出現回数を数える場合とほぼ同じです。例えば、文字ごとの出現位置の記録などで使うことができます。
or_insert()
を使うと、
let mut map = std::collections::HashMap::new();
for (i, c) in "abcabc".chars().enumerate() {
map.entry(c).or_insert(Vec::new()).push(i);
}
println!("{:?}", map); // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
また、型推論ができていれば、or_default()
でもできます。
let mut map = std::collections::HashMap::<_, Vec<_>>::new();
for (i, c) in "abcabc".chars().enumerate() {
map.entry(c).or_default().push(i);
}
println!("{:?}", map); // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
そして、and_modify()
でも記述できます。この場合でもなぜか型を明示する必要があるようです。(or_insert で Vec を与えているのに型推論はしてくれない(?))
let mut map = std::collections::HashMap::<_, Vec<_>>::new();
for (i, c) in "abcabc".chars().enumerate() {
map.entry(c).and_modify(|e| e.push(i)).or_insert(vec![i]);
}
println!("{:?}", map); // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
副作用のある処理をデフォルトで行いたい場合
当然なのかもしれませんが、entry().or_insert()
の行が実行されるたびor_insert()
の引数に与えている部分が評価されるようです。そのため、副作用のある処理をor_insert()
の中に書いてしまうと、挿入が発生しなくても初期化処理の部分が実行されて、期待している動作をしないことがあります。
そのような場合は、or_insert_with()
が使えます。or_insert_with
これは引数にデフォルト値を返す関数を与えるため、挿入が発生して初めて実行されます。
あとがき
自分は Rust にあまり詳しいわけではないので、間違いや抜けがあった場合はぜひコメントなどで指摘をお願い致します。