Fix Rust 'the trait bound f64: Hash is not satisfied' khi dùng số thực làm key HashMap

intermediate🦀 Rust2026-06-01| Rust 1.x, mọi nền tảng (Linux, macOS, Windows), dự án dùng std::collections::HashMap với key f64 hoặc f32

Error Message

error[E0277]: the trait bound `f64: Hash` is not satisfied
#rust#hashmap#f64#hash-trait

Lỗi Gặp Phải

Bạn đã thử dùng số thực dấu phẩy động làm key trong HashMap và gặp lỗi sau tại thời điểm biên dịch:

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`

Tại Sao f64 Không Thể Dùng Làm Key của HashMap

Rust yêu cầu key của HashMap phải implement hai trait: HashEq. Các kiểu số thực dấu phẩy động (f32f64) không implement cả hai — và điều đó có lý do chính đáng.

Vấn đề nằm ở NaN. Chuẩn IEEE 754 quy định rằng NaN != NaN, điều này vi phạm trực tiếp hợp đồng mà Eq yêu cầu (mỗi giá trị phải bằng chính nó). Và vì không thể có một implementation Eq đúng đắn, bạn cũng không thể có implementation Hash đúng đắn — một key không bằng chính nó sẽ phá vỡ toàn bộ bất biến của bảng băm.

Vì vậy Rust đơn giản là không implement các trait này cho kiểu float. Đây không phải giới hạn có thể vượt qua bằng feature flag; đây là lựa chọn thiết kế có chủ đích để ngăn chặn lỗi dữ liệu âm thầm.

Cách Sửa 1: Dùng Crate ordered_float (Khuyến nghị)

Giải pháp gọn nhất cho hầu hết các trường hợp là crate ordered_float. Crate này cung cấp hai kiểu wrapper:

  • OrderedFloat<f64> — chấp nhận mọi giá trị float kể cả NaN (coi tất cả NaN là bằng nhau)
  • NotNan<f64> — panic hoặc trả về lỗi nếu bạn cố lưu NaN

Thêm vào Cargo.toml của bạn:

[dependencies]
ordered-float = "4"

Sau đó dùng làm kiểu key:

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")
}

Dùng NotNan khi NaN thực sự là trạng thái không hợp lệ trong domain của bạn — đây là lựa chọn an toàn hơn và khiến trạng thái không hợp lệ trở nên không thể biểu diễn được. Dùng OrderedFloat nếu bạn thực sự cần lưu trữ các key NaN.

Cách Sửa 2: Chuyển Sang Bits và Dùng u64 Làm Key

Nếu bạn kiểm soát được code và chắc chắn các giá trị không bao giờ là NaN hay -0.0, bạn có thể chuyển đổi f64 sang biểu diễn bit thô (u64) và dùng đó làm key:

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")
}

Một điểm cần lưu ý: 0.0-0.0 bằng nhau theo so sánh IEEE 754 nhưng có biểu diễn bit khác nhau. Nếu sự khác biệt đó quan trọng trong map của bạn, bạn cần chuẩn hóa chúng trước:

fn f64_key(v: f64) -> u64 {
    assert!(!v.is_nan());
    // Treat +0.0 and -0.0 as the same key
    if v == 0.0 { 0u64.to_bits() } else { v.to_bits() }
}

Cách Sửa 3: Wrapper Tùy Chỉnh với Hash + Eq Thủ Công

Nếu bạn muốn toàn quyền kiểm soát và không muốn phụ thuộc bên ngoài, bạn có thể viết một wrapper đơn giản:

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) {
        // Chuẩn hóa tương tự cách sửa 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")
}

Cách này hoạt động, nhưng đây là boilerplate mà bạn phải tự chịu trách nhiệm bảo trì. Crate ordered_float làm điều này (và làm cẩn thận hơn) thay cho bạn.

Cách Sửa 4: Xem Xét Lại Kiểu Key

Đây là cách sửa thực sự áp dụng phổ biến nhất. Nếu bạn đang xây dựng map với key là số thực dấu phẩy động, hãy tự hỏi: liệu float có thực sự là kiểu key phù hợp ở đây không?

  • Bin tần số → dùng chỉ số bin nguyên thay vì tần số trung tâm
  • Giá tiền → dùng số nguyên xu/satoshi thay vì đồng đô la
  • Tọa độ → làm tròn đến độ chính xác cố định và dùng key dạng chuỗi hoặc số nguyên
  • Tra cứu theo giá trị cảm biến → phân nhóm các giá trị vào các khoảng trước

Phép so sánh số thực dấu phẩy động vốn dĩ có tính mơ hồ, vì vậy một map có key là giá trị float chính xác thường là dấu hiệu cho thấy thiết kế cần được xem xét lại.

Kiểm Tra Sau Khi Sửa

Sau khi áp dụng bất kỳ cách sửa nào ở trên, hãy chạy:

cargo build

Lỗi E0277 sẽ biến mất. Sau đó chạy các bài test của bạn:

cargo test

Thêm một kiểm tra nhanh nếu bạn chưa có:

#[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);
    }
}

Mẹo

Nếu bạn đang debug lý do tại sao hai giá trị float trông có vẻ bằng nhau lại không khớp khi làm key trong map, việc kiểm tra biểu diễn bit thô của chúng sẽ rất hữu ích. Bạn có thể dùng println!("{:064b}", my_f64.to_bits()) trong Rust, hoặc để kiểm tra nhanh bên ngoài code, dùng công cụ kiểm tra hash/bit như Hash Generator của ToolCraft — hữu ích để xác minh rằng hai chuỗi byte thực sự giống hệt nhau khi bạn không chắc liệu giá trị float có đang được serialize nhất quán hay không.

Cũng cần biết: vấn đề tương tự cũng áp dụng cho f32. Crate ordered_float hỗ trợ cả OrderedFloat<f32>NotNan<f32>.

Cuối cùng, nếu bạn đến từ Python hay JavaScript nơi điều này hoạt động bình thường, hãy nhớ rằng những ngôn ngữ đó đang thực hiện một lựa chọn ngầm và có thể gây lỗi cho bạn. Rust buộc bạn phải tường minh, điều này giúp tránh những lỗi key bị trùng âm thầm trong môi trường production.

Related Error Notes