エラーの内容
HashMap のキーに浮動小数点数を使おうとすると、コンパイル時に次のエラーが発生します:
error[E0277]: the trait bound `f64: Hash` is not satisfied
--> src/main.rs:5:36
|
5 | let mut map: HashMap<f64, i32> = HashMap::new();
| ^^^^^^^^^^^^^^^ the trait `Hash` is not implemented for `f64`
|
= help: the following other types implement trait `Hash`: ...
note: required by a bound in `HashMap`
なぜ f64 を HashMap のキーにできないのか
Rust の HashMap は、キーに Hash と Eq という2つのトレイトの実装を要求します。浮動小数点型(f32 および f64)は、そのどちらも実装していません — これには十分な理由があります。
問題は NaN です。IEEE 754 では NaN != NaN と規定されており、これは「すべての値は自身と等しくなければならない」という Eq の契約に直接違反します。健全な Eq 実装が不可能であれば、健全な Hash 実装も不可能です — 自身と等しくないキーは、ハッシュテーブル全体の不変条件を壊してしまいます。
そのため Rust は浮動小数点型にこれらのトレイトを実装していません。これはフィーチャーフラグで回避できる制限ではなく、サイレントなデータ破壊を防ぐための意図的な設計上の選択です。
修正方法 1:ordered_float クレートを使う(推奨)
ほとんどのケースで最もすっきりした解決策は、ordered_float クレートです。このクレートは2つのラッパー型を提供します:
OrderedFloat<f64>—NaNを含む任意の浮動小数点数を許可(すべての NaN を等しいものとして扱う)NotNan<f64>—NaNを格納しようとするとパニックまたはエラーを返す
Cargo.toml に追加します:
[dependencies]
ordered-float = "4"
その後、キーの型として使用します:
use ordered_float::NotNan;
use std::collections::HashMap;
fn main() {
let mut map: HashMap<NotNan<f64>, &str> = HashMap::new();
let key = NotNan::new(3.14).expect("key is NaN");
map.insert(key, "pi");
let lookup = NotNan::new(3.14).unwrap();
println!("{:?}", map.get(&lookup)); // Some("pi")
}
NaN がドメインとして本当に無効な状態である場合は NotNan を使いましょう — より安全な選択肢であり、無効な状態を表現不可能にします。NaN キーを正当に格納する必要がある場合は OrderedFloat を使います。
修正方法 2:ビット列に変換して u64 をキーとして使う
コードを自分で管理していて、値が絶対に NaN や -0.0 にならないと確信できる場合は、f64 を生のビット表現(u64)に変換してキーとして使用できます:
use std::collections::HashMap;
fn f64_key(v: f64) -> u64 {
assert!(!v.is_nan(), "NaN cannot be used as a map key");
v.to_bits()
}
fn main() {
let mut map: HashMap<u64, &str> = HashMap::new();
map.insert(f64_key(1.5), "one and a half");
map.insert(f64_key(-0.75), "negative three quarters");
println!("{:?}", map.get(&f64_key(1.5))); // Some("one and a half")
}
注意点として、0.0 と -0.0 は IEEE 754 の比較では等しいですが、異なるビット表現を持ちます。マップ上でその区別が重要な場合は、事前に正規化する必要があります:
fn f64_key(v: f64) -> u64 {
assert!(!v.is_nan());
// +0.0 と -0.0 を同じキーとして扱う
if v == 0.0 { 0u64.to_bits() } else { v.to_bits() }
}
修正方法 3:Hash + Eq を手動実装したカスタムラッパーを作る
外部依存を避けて完全に制御したい場合は、薄いラッパーを自分で書くことができます:
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Clone, Copy, PartialEq)]
struct FloatKey(f64);
impl Eq for FloatKey {}
impl Hash for FloatKey {
fn hash<H: Hasher>(&self, state: &mut H) {
// 修正方法 2 と同じ正規化
let bits = if self.0.is_nan() {
f64::NAN.to_bits()
} else if self.0 == 0.0 {
0u64
} else {
self.0.to_bits()
};
bits.hash(state);
}
}
fn main() {
let mut map: HashMap<FloatKey, &str> = HashMap::new();
map.insert(FloatKey(2.718), "euler");
println!("{:?}", map.get(&FloatKey(2.718))); // Some("euler")
}
これは動作しますが、今後はあなた自身がこのボイラープレートを保守し続けなければなりません。ordered_float クレートはこれを(より丁寧に)代わりにやってくれます。
修正方法 4:キーの型を見直す
これが実際に最も多く当てはまる修正方法です。浮動小数点数をキーとするマップを構築しているなら、自問してみてください:本当に浮動小数点数が正しいキーなのでしょうか?
- 周波数ビン → 中心周波数の代わりに整数のビンインデックスを使う
- 価格 → ドルではなく整数のセント/サトシを使う
- 座標 → 固定精度に丸めて文字列または整数キーを使う
- センサー値による検索 → 先に値を範囲でバケット化する
浮動小数点数の等値比較は本質的にあいまいなため、正確な浮動小数点値をキーとするマップは、設計を見直す余地があるサインであることが多いです。
修正の確認
上記の修正方法のいずれかを適用したら、次を実行します:
cargo build
E0277 エラーが消えます。続いてテストを実行します:
cargo test
まだ追加していない場合は、簡単なサニティチェックを加えておきましょう:
#[cfg(test)]
mod tests {
use super::*;
use ordered_float::NotNan;
use std::collections::HashMap;
#[test]
fn float_key_roundtrip() {
let mut map: HashMap<NotNan<f64>, i32> = HashMap::new();
let k = NotNan::new(1.23).unwrap();
map.insert(k, 42);
assert_eq!(map[&NotNan::new(1.23).unwrap()], 42);
}
}
補足情報
見た目上は等しい2つの浮動小数点値がマップキーとして一致しない原因をデバッグしている場合、生のビット表現を確認すると役立ちます。Rust では println!("{:064b}", my_f64.to_bits()) を使うか、コード外での簡易チェックには ToolCraft の Hash Generator のようなハッシュ/ビットインスペクタが使えます — 浮動小数点値が一貫してシリアライズされているか確信が持てないときに、2つのバイト列が本当に同一かどうかを確認するのに便利です。
また覚えておきたいのは、この同じ問題が f32 にも当てはまるということです。ordered_float クレートは OrderedFloat<f32> と NotNan<f32> の両方をサポートしています。
最後に、Python や JavaScript ではこれが普通に動作するという経験をお持ちの方へ:それらの言語は暗黙的に、そして場合によってはバグを含む選択をあなたの代わりに行っています。Rust は明示的に記述することを求めますが、それによって本番環境での微妙なキー衝突バグから守られます。

