我认为解释为什么比较可能不明智很重要,因为根据您的用例,您可能不关心这些问题。
导致诸如(fat)指针比较被认为是反模式的一个简单原因是,这样的比较可能会产生令人惊讶的结果;对于布尔测试,只有两种非直观情况:
误报(两个预期的不同事物仍然相等);
漏报(两个预期的相等的事物最终不相等)。
显然,所有这些都与 期望 相关。执行的测试是指针等式:
大多数人会期望如果两个指针指向相同的数据,则它们应该相等... 但在涉及 fat 指针时并不一定如此。这肯定会导致漏报。
一些人,特别是那些不习惯零大小类型的人,可能还会想象两个不同的实例必须必然位于不同的内存中,并且“因此”具有不同的地址。但是(零大小的)类型即使位于相同的地址上,也不可能重叠,因为这种重叠是零大小的!这意味着您可以在相同的地址上拥有“不同的此类实例”(顺便说一下,这也是许多语言不支持零大小类型的原因:失去了独特地址属性会有自己的缺点)。不了解此情况的人可能会观察到误报。
例子
两个fat指针可以具有相等的数据指针,但比较结果却不相等
There is a very basic example of it. Consider:
let arr = [1, 2, 3];
let all = &arr[..];
let first = &arr[.. 1];
assert!(::core::ptr::eq(all, first));
This is a basic example where we can see that the extra metadata bundled within a fat pointer (hence their being dubbed "fat") may vary "independently" of the data pointer, leading to these fat pointers then comparing unequal.
现在,语言中仅有的另一种使用fat pointers的情况是(指向)dyn Trait/特质对象。这些fat pointers携带的元数据是一个指向包含主要特定方法(fn指针)的结构体的引用,该结构体对应于数据的已擦除的原始类型:虚拟方法表,即vtable。
每当将(因此变得苗条的)指向具体类型的指针强制转换为fat pointer时,编译器会自动产生这样的引用:
&42_i32
as &dyn Display
原来编译器(更准确地说,是当前编译单元)这样做时,它会创建自己的虚表。
这意味着如果不同的编译单元执行这样的强制转换,则可能涉及多个虚表,因此对它们的引用可能彼此不相等!
我确实能够在以下游乐场中重现这一点,该游乐场利用了src/{lib,main}.rs
分别编译的事实:
在我撰写本文时,该游乐场失败并显示如下错误信息:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0x5567e54f4047`,
right: `0x5567e54f4047`', src/main.rs:14:9
如您所见,数据指针是相同的,而assert_eq!
错误消息只显示这些内容(fat指针的Debug
实现不显示元数据)。
两个不同的对象可能存在于相同的地址上
非常简单的展示:
let box1 = Box::new(());
let box2 = Box::new(());
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);
assert_eq!(at_box1, at_box2);
assert_eq!(at_vec0, at_box1);
结论?
现在你已经了解了“胖指针比较”的注意事项,如果您想要执行比较(明知这违反了Clippy lint的建议),您可以做出选择,例如,在您的代码中的单个位置将所有的Arc<dyn Trait>
实例"肥化"(强制转换为dyn
) (这可以避免来自不同虚表的误报),并且没有涉及到零大小的实例(这可以避免误报)。
例如:
mod lib {
use ::std::rc::Rc;
pub
trait MyTrait { }
impl MyTrait for i32 { }
impl MyTrait for String { }
#[derive(Clone)]
pub
struct MyType {
private: Rc<dyn 'static + MyTrait>,
}
impl MyType {
fn new<T : 'static + MyTrait> (instance: T)
-> Option<Self>
{
if ::core::mem::size_of::<T>() == 0 {
None
} else {
Some(Self { private: Rc::new(instance) })
}
}
pub fn new_i32(i: i32) -> Option<Self> { Self::new(i) }
pub fn new_string(s: String) -> Option<Self> { Self::new(s) }
pub
fn ptr_eq (self: &'_ MyType, other: &'_ MyType)
-> bool
{
::core::ptr::eq(&*self.private, &*other.private)
}
}
}
不过,正如您所看到的,即使那时我似乎仍然依赖于关于当前虚表实例化的一些知识; 因此这是相当不可靠的。这就是为什么您应该仅执行Slim指针比较的原因。
TL,DR
首先将每个指针变薄 (&*arc as *const _ as *const ()
):然后才有意义去比较指针。
Arc
规则排除了零大小类型和常见初始序列,但repr(transparent)
仍然适用。我认为“相同”是什么意思很重要。 - trent