4.7.4 dyn PartialEq

译文 · 基于 Learning Rust

dyn PartialEq

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

现在让我们转向稍复杂的内容。我们之前提到,Self 不能在接收者之外使用,例如作为另一个参数,因为无法保证另一个参数与接收者具有相同的基类型(若基类型不同,则没有可调用的实际实现)。

让我们看看如何绕过此限制,为 dyn Trait 实现 PartialOrd,尽管存在 &Self 参数。在类型擦除背景下,该 trait 很合适,因为当类型不匹配时我们可以返回 None,表示无法比较。

PartialOrd 要求 PartialEq,因此我们也会处理后者。

使用 dyn Any 向下转型以模拟动态类型

我们在先前示例中不必使用 dyn Any,因为我们能够将实现安排得使动态分发隐式地将擦除类型「向下转型」为具体基类型。它能做到这一点,是因为指向基类型的指针与只接受该基类型的 vtable 耦合,无需真正的动态类型或运行时类型比较。对这些情况,转换是不可失败的。

然而,现在我们有两个宽指针,可能指向不同的基类型。在此特定应用中,我们实际上只需要知道它们是否具有相同的基类型……尽管若能安全地恢复非接收者的擦除类型也会很好,而不是依赖各种强制转换技巧。

你可能认为可以用 vtable 指针来判断基类型是否相同。但不幸的是,我们不能依赖 vtable 在运行时比较类型。

比较宽指针时,地址和元数据都会测试相等性。然而请注意,比较 trait 对象指针(*const dyn Trait)是不可靠的:指向相同底层类型值的指针可能比较不相等(因为 vtable 在多个代码生成单元中重复),指向不同底层类型值的指针可能比较相等(因为相同 vtable 可在代码生成单元内去重)。

没错,假阴性和假阳性都有。有趣!

因此我们需要另一种机制来比较类型并判断两个宽指针是否指向相同基类型,这就是 dyn Any 的用武之地。Any 是用于模拟动态类型的 trait,并为 dyn Any、Box<dyn Any + Send> 等类型擦除形式提供了许多可失败的向下转型方法。这不仅能比较基类型是否相等,还能安全地恢复擦除的基类型(「向下转型」)。

Any trait 出于健全性带有 'static 约束,因此注意本示例的基类型会更受限。

此外,我们将依赖相对较新的超 trait 向上转型能力以提高易用性。若你的 MSRV 不允许,示例仍可行,但需要自行提供向上转型能力。 本章末尾展示了那会是什么样子。

最后旁注,我们稍后会更详细地探讨 dyn Any。

元讨论够了,开始吧!

PartialEq

基本思路是:我们将有一个比较 trait DynCompare,然后以通用方式为 dyn DynCompare 实现 PartialEq。然后我们的实际 trait(Trait)可以将 DynCompare 作为超 trait,并通过向上转型到 dyn DynCompare 为 dyn Trait 实现 PartialEq。

在 dyn DynCompare 的实现中,我们必须(尝试)向下转型到擦除的基类型。为此,我们首先需要从 dyn DynCompare 向上转型到 dyn Any。

在 trait 向上转型之前,我们会再次使用「可 blanket 实现的超 trait」模式。但在现代 Rust 中,我们只需在 DynCompare 上设置超 trait 约束。

1
2
3
4
5
use std::any::Any;

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

Any: 'static 约束适用于 dyn Any + '_,因此 &dyn Any 实际上总是 &(dyn Any + 'static)。 由于 DynCompare 上的 Any 超 trait,&dyn DynCompare 也具有「总是 'static」属性。好处是我们完全不必操心 dyn 生命周期的灵活性——它总是 'static。

缺点是只有满足 'static 约束的基类型才能得到支持,因此在某些小众情况下你可能不想包含超 trait 约束。然而,既然需要向上转型到 dyn Any,就意味着你在假装是另一种类型,这似乎确实很小众。若为自己的用例尝试非 'static 路线,注意本示例中某些实现可以更通用;你还需要其他方法支持向上转型。

不管怎样,继续跨类型相等性比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# use std::any::Any;
# trait DynCompare: Any {
#    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
# }
impl<T: Any + PartialEq> 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
        }
    }
}

// 注:当 trait 对象生命周期未约束为 `'static` 时,
// 这可以以更通用的方式实现
impl PartialEq<dyn DynCompare> for dyn DynCompare {
    fn eq(&self, other: &dyn DynCompare) -> bool {
        self.dyn_eq(other)
    }
}

我们利用 dyn Any 向上转型尝试恢复自己基类型的参数,若成功则进行实际(部分)比较。否则判定不相等。

这使我们能为 dyn Compare 实现 PartialEq。

然后我们将此功能接入实际 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
# use std::any::Any;
# trait DynCompare: Any {
#    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
# }
# impl<T: Any + PartialEq> 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
#         }
#     }
# }
# impl PartialEq<dyn DynCompare> for dyn DynCompare {
#     fn eq(&self, other: &dyn DynCompare) -> bool {
#         self.dyn_eq(other)
#     }
# }
trait Trait: DynCompare {}
impl Trait for i32 {}
impl Trait for bool {}

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

超 trait 约束完成了大部分工作,我们再次使用向上转型——这次是到 dyn DynCompare——以便对 dyn Trait 执行 PartialEq。

std 中的 blanket 实现 自动为我们提供 Box<dyn Trait> 的 PartialEq。

现在试试:

 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
# use std::any::Any;
# trait DynCompare: Any {
#    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
# }
# impl<T: Any + PartialEq> 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
#         }
#     }
# }
# impl PartialEq<dyn DynCompare> for dyn DynCompare {
#     fn eq(&self, other: &dyn DynCompare) -> bool {
#         self.dyn_eq(other)
#     }
# }
# trait Trait: DynCompare {}
# impl Trait for i32 {}
# impl Trait for bool {}
# impl PartialEq<dyn Trait> for dyn Trait {
#     fn eq(&self, other: &dyn Trait) -> bool {
#         (self as &dyn DynCompare) == (other as &dyn DynCompare)
#     }
# }
fn main() {
    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);

    println!("{}", bx1a == bx1a);
    println!("{}", bx1a == bx1b);
    println!("{}", bx1a == bx2);
    println!("{}", bx1a == bx3);
}

呃……没成功,但原因很奇怪。为什么比较时要尝试从 Box 移出?原来这是语言中的一个长期存在的 bug。 幸运的是该 issue 也提供了在使用点很易用的变通办法:同时实现 PartialEq<&Self>。

 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
# use std::any::Any;
# trait DynCompare: Any {
#    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
# }
# impl<T: Any + PartialEq> 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
#         }
#     }
# }
# impl PartialEq<dyn DynCompare> for dyn DynCompare {
#     fn eq(&self, other: &dyn DynCompare) -> bool {
#         self.dyn_eq(other)
#     }
# }
# trait Trait: DynCompare {}
# impl Trait for i32 {}
# impl Trait for bool {}
# impl PartialEq<dyn Trait> for dyn Trait {
#     fn eq(&self, other: &dyn Trait) -> bool {
#         (self as &dyn DynCompare) == (other as &dyn DynCompare)
#     }
# }
// 新增
impl PartialEq<&Self> for Box<dyn Trait> {
    fn eq(&self, other: &&Self) -> bool {
        <Self as PartialEq>::eq(self, *other)
    }
}

fn main() {
    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);

    println!("{}", bx1a == bx1a);
    println!("{}", bx1a == bx1b);
    println!("{}", bx1a == bx2);
    println!("{}", bx1a == bx3);
}

好了,现在可以了。呼!

PartialOrd

从这里添加 PartialOrd 支持大多是机械性的:

 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
+use core::cmp::Ordering;

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

-impl<T: Any + PartialEq> DynCompare for T {
+impl<T: Any + PartialOrd> 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 PartialOrd<dyn DynCompare> for dyn DynCompare {
+    fn partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
+        self.dyn_partial_cmp(other)
+    }
+}

+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 PartialOrd<&Self> for Box<dyn Trait> {
+    fn partial_cmp(&self, other: &&Self) -> Option<Ordering> {
+        <Self as PartialOrd>::partial_cmp(self, *other)
+    }
+}

这是最终的 playground。

与手动超 trait 向上转型的对比

这是使用手动超 trait 向上转型的 playground。

样板代码更多,但可以说方法调用更易用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
impl PartialEq<dyn Trait> for dyn Trait {
    fn eq(&self, other: &dyn Trait) -> bool {
        // (self as &dyn DynCompare) == (other as &dyn DynCompare)
        self.as_dyn_compare() == other.as_dyn_compare()
    }
}

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

根据 trait 的使用方式,在内置 trait 向上转型之上提供 AsDynCompare 方法是否值得,可能因情况而异。它不必再是超 trait,可以独立于上面探讨的内置 trait 向上转型实现来完成。

最后修改 August 23, 2026: 更新 (499855b16)