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 的实现重叠。
见该小节最后一段。