4.7.7 可哈希的 Box

译文 · 基于 Learning Rust

可哈希的 Box

原文链接: https://quinedot.github.io/rust-learning/dyn-trait-hash.html

假设我们有一个实现了 Eq 的 dyn Trait,但我们还希望它实现 Hash,以便能在 HashSet 中使用 Box<dyn Trait>,或作为 HashMap 的键等。

这是我们的起点

与之前相比,唯一更新是要求 Eq:

1
2
3
4
5
-impl<T: Any + PartialOrd> DynCompare for T {
+impl<T: Any + PartialOrd + Eq> DynCompare for T {

+impl Eq for dyn DynCompare {}
+impl Eq for dyn Trait {}

完整代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
use core::cmp::Ordering;
use std::any::Any;

trait DynCompare: Any {
    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
    fn dyn_partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering>;
}

impl<T: Any + PartialOrd + Eq> DynCompare for T {
    fn dyn_eq(&self, other: &dyn DynCompare) -> bool {
        if let Some(other) = (other as &dyn Any).downcast_ref::<Self>() {
            self == other
        } else {
            false
        }
    }

    fn dyn_partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
        (other as &dyn Any)
            .downcast_ref::<Self>()
            .and_then(|other| self.partial_cmp(other))
    }
}

impl Eq for dyn DynCompare {}
impl PartialEq<dyn DynCompare> for dyn DynCompare {
    fn eq(&self, other: &dyn DynCompare) -> bool {
        self.dyn_eq(other)
    }
}

impl PartialOrd<dyn DynCompare> for dyn DynCompare {
    fn partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
        self.dyn_partial_cmp(other)
    }
}

trait Trait: DynCompare {}
impl Trait for i32 {}
impl Trait for bool {}


impl Eq for dyn Trait {}
impl PartialEq<dyn Trait> for dyn Trait {
    fn eq(&self, other: &dyn Trait) -> bool {
        (self as &dyn DynCompare) == (other as &dyn DynCompare)
    }
}

impl PartialOrd<dyn Trait> for dyn Trait {
    fn partial_cmp(&self, other: &dyn Trait) -> Option<Ordering> {
        (self as &dyn DynCompare).partial_cmp(other as &dyn DynCompare)
    }
}

impl PartialEq<&Self> for Box<dyn Trait> {
    fn eq(&self, other: &&Self) -> bool {
        <Self as PartialEq>::eq(self, *other)
    }
}

impl PartialOrd<&Self> for Box<dyn Trait> {
    fn partial_cmp(&self, other: &&Self) -> Option<Ordering> {
        <Self as PartialOrd>::partial_cmp(self, *other)
    }
}

与我们讨论 Clone 时类似,Hash 不是对象安全的 trait。因此我们不能简单地将 Hash 作为超 trait 约束添加。这次不是因为需要 Sized,而是因为它有泛型方法:

1
2
3
4
5
# use core::hash::Hasher;
pub trait Hash {
    fn hash<H>(&self, state: &mut H)
       where H: Hasher;
}

幸运的是,我们可以使用擦除 trait 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::collections::HashSet;
use core::hash::{Hash, Hasher};

trait DynHash {
    fn dyn_hash(&self, state: &mut dyn Hasher);
}

// impl<T: ?Sized + Hash> DynHash for T {
impl<T: Hash> DynHash for T {
    fn dyn_hash(&self, mut state: &mut dyn Hasher) {
        self.hash(&mut state)
    }
}

impl Hash for dyn DynHash + '_ {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.dyn_hash(state)
    }
}

此时值得指出我们依赖的几点:

  • Hasher 是对象安全的。

    若不是这样,我们就无法在 dyn_hash 方法中接受 &mut dyn Hasher。

  • Hash::hash<H: Hasher> 中的泛型 H 有隐式 Sized 约束
    若不是这样,我们就无法在 dyn DynHash 的实现中将 &mut H 强制转换为 &mut dyn Hasher。我们也不会完全无路可走——此递归定义意味着我们可以将 &mut &mut H 强制转换为 &mut dyn Hasher,代价是一些间接层。

    然而,这确实表明放宽 Sized 约束可能是破坏性变更!

继续,我们仍需将此新功能接入我们自己的 trait。

1
2
-trait Trait: DynCompare;
+trait Trait: DynCompare + DynHash {}
1
2
3
4
5
6
7
8
9
# use core::hash::{Hash, Hasher};
# trait DynHash { fn dyn_hash(&self, _: &mut dyn Hasher); }
# trait Trait: DynHash {}
// 与我们对 `dyn DynHash` 所做的一样
impl Hash for dyn Trait {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.dyn_hash(state)
    }
}

就是这样!

1
2
3
4
5
6
7
    let bx1a: Box<dyn Trait> = Box::new(1);
    let bx1b: Box<dyn Trait> = Box::new(1);
    let bx2: Box<dyn Trait> = Box::new(2);
    let bx3: Box<dyn Trait> = Box::new(true);

    let hm: HashSet<_> = HashSet::from_iter([bx1a, bx1b, bx2, bx3].into_iter());
    assert_eq!(hm.len(), 3);

结语

若适用,借用具体类型 可能是更好的方法,因为它不需要 Any + 'static。

尽管效率极低,对所有内容返回相同哈希值的 Hash 实现也是正确实现。其他「粗略近似」亦然,例如只哈希 TypeId。逻辑行为所要求的只是两个相等的值必须有相等的哈希。因此可以说,我们不必费尽周折去得到值的精确哈希。

你可能注意到这行被注释掉了:

1
2
// impl<T: ?Sized + Hash> DynHash for T {
impl<T: Hash> DynHash for T {

我选择较不通用实现的原因有二:

  • 示例不需要更通用版本
  • 审慎:因为我们 impl Hash for dyn DynHash,技术上与编译器对 DynHash for dyn DynHash 的实现重叠。 见该小节最后一段。
最后修改 August 23, 2026: 更新 (499855b16)