假设我有以下上下文:
let a : Arc<dyn SomeTrait> = getA();
let b : Arc<dyn SomeTrait> = getB();
Run Code Online (Sandbox Code Playgroud)
现在,我想知道是否a
和b
持有同一个对象,但以下两种方法被 Clippy 标记为comparing trait object pointers compares a non-unique vtable address
:
let eq1 = std::ptr::eq(a.as_ref(), b.as_ref());
let eq2 = Arc::ptr_eq(&a, &b);
Run Code Online (Sandbox Code Playgroud)
检查 trait 对象相等性的推荐方法是什么?
我认为解释为什么比较可能是不明智的很重要,因为根据您的用例,您可能不会关心这些问题。
像(胖)指针比较这样简单的事情被认为是反模式的主要原因是这样的比较可能会产生令人惊讶的结果。对于布尔测试,只有两种不直观的情况:
误报(两个预期的不同事物仍然比较相等);
假阴性(两个预期相等的东西最终不会比较相等)。
显然这里都与期待有关。执行的测试是指针相等:
大多数人会期望,如果两个指针指向相同的数据,那么它们应该比较相等……在谈论胖指针时,情况不一定如此。因此,这肯定会导致假阴性。
一些人,尤其是那些不习惯零大小类型的人,也可能认为两个不同的实例必然存在于不相交的内存中,并且“因此”具有不同的地址。但是(实例)零大小的类型不可能重叠,即使它们住在同一个地址,因为这样的重叠是零大小的!这意味着您可以在同一地址拥有“不同的此类实例”(顺便说一句,这也是许多语言不支持零大小类型的原因:丢失唯一地址的这种属性有其自身的警告)。不知道这种情况的人可能因此观察到误报。
有一个非常基本的例子。考虑:
let arr = [1, 2, 3];
let all = &arr[..]; // len = 3, data_ptr = arr.as_ptr()
let first = &arr[.. 1]; // len = 1, data_ptr = arr.as_ptr()
assert!(::core::ptr::eq(all, first)); // Fails!
Run Code Online (Sandbox Code Playgroud)
这是一个基本示例,我们可以看到捆绑在胖指针中的额外元数据(因此被称为“胖”)可能与数据指针“独立”变化,导致这些胖指针比较不相等。
现在,语言中胖指针的唯一其他实例是(指向)dyn Trait
s / trait 对象。这些胖指针携带的元数据是对结构的引用,该结构主要包含fn
与现在擦除的原始数据类型相对应的特征的特定方法(作为指针):虚拟方法表,也就是虚表。
每次将指向具体类型的(如此纤细的)指针强制转换为胖指针时,编译器都会自动生成这样的引用:
&42_i32 // (slim) pointer to an integer 42
as &dyn Display // compiler "fattens" the pointer by embedding a
// reference to the vtable of `impl Display for i32`
Run Code Online (Sandbox Code Playgroud)
事实证明,当编译器,或更准确地说,当前编译单元执行此操作时,它会创建自己的 vtable。
这意味着如果不同的编译单元执行这样的强制转换,可能会涉及到多个 vtable,因此对它们的引用可能并不完全相等!
我确实能够在以下操场中重现这一点,(ab)使用src/{lib,main}.rs
单独编译的事实。
在我写这篇文章的时候,那个 Playground 失败了:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
Run Code Online (Sandbox Code Playgroud)
如您所见,数据指针是相同的,assert_eq!
错误消息仅显示那些(Debug
胖指针的impl 不显示元数据)。
非常简单的展示:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
Run Code Online (Sandbox Code Playgroud)
既然您知道了胖指针比较的注意事项,那么您仍然可以选择执行比较(有意使 Clippy lint 静音),例如,如果您的所有Arc<dyn Trait>
实例都dyn
在您的代码(这避免了来自不同 vtable 的误报),并且如果不涉及零大小的实例(这避免了误报)。
例子:
let box1 = Box::new(()); // zero-sized "allocation"
let box2 = Box::new(()); // ditto
let vec = vec![(), ()];
let at_box1: *const () = &*box1;
let at_box2: *const () = &*box2;
let at_vec0: *const () = &vec[0];
let at_vec1: *const () = &vec[1];
assert_eq!(at_vec0, at_vec1); // Guaranteed.
assert_eq!(at_box1, at_box2); // Very likely.
assert_eq!(at_vec0, at_box1); // Likely.
Run Code Online (Sandbox Code Playgroud)
但是,正如您所看到的,即便如此,我似乎还是依赖于有关vtable 实例化的当前实现的一些知识;因此,这是非常不可靠的。这就是为什么您应该只执行细长指针比较的原因。
首先细化每个指针 ( &*arc as *const _ as *const ()
):只有这样比较指针才是明智的。