如何使用通用方法参数创建可哈希的特征对象/特征对象?

Mar*_*ark 5 generics traits rust

我有一些同时实现Hash和的结构MyTrait。我将它们用作&MyTrait特征对象。

现在我&MyTrait也想实施Hash。我已经尝试了几件事:

  • 天真地trait MyTrait: Hash {}

    the trait `MyTrait` cannot be made into an object
    
    Run Code Online (Sandbox Code Playgroud)
  • 然后我尝试了这个:

    impl Hash for MyTrait {
        fn hash<H: Hasher>(&self, hasher: &mut H) {
            // ...
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    但是我认为我需要委托给hash具体类型的方法self

  • 因此,天真的下一步是戴上它MyTrait

    fn my_hash<H: Hasher>(&self, hasher: &mut H);
    
    Run Code Online (Sandbox Code Playgroud)

    这使我回到了第一点。

  • 我读了一些有关使用特征对象而不是通用参数的内容,这听起来很聪明,因此我将其放在 MyTrait

    fn my_hash(&self, hasher: &mut H);
    
    Run Code Online (Sandbox Code Playgroud)

    然后,我需要实际执行此操作。最好不要用手为每个特征:

    impl<T: 'static + Hash> MyTrait for T {
        fn as_any(&self) -> &Any {
            self as &Any
        }
    
        fn my_hash(&self, hasher: &mut Hasher) {
            self.as_any().downcast_ref::<T>().unwrap().hash(hasher)
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    但是之后

    impl Hash for MyTrait {
        fn hash<H: Hasher>(&self, hasher: &mut H) {
            // ...
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    所以我不得不垂头丧气Hasher……

  • 如果使用向下转换,Hasher则需要一个H可以转换为的通用参数Any Hasher,让我们尝试:

    trait AnyHasher {
        fn as_any(&self) -> &Any;
    }
    
    impl<H: 'static + Hasher> AnyHasher for H {
        fn as_any(&self) -> &Any {
            self as &Any
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    然后垂头丧气

    impl<T: 'static + Hash, H: 'static + Hasher> MyTrait for T {
        // ...
        fn my_hash(&self, hasher: &mut AnyHasher) {
            let h = hasher.as_any().downcast_ref::<H>().unwrap();
            self.as_any().downcast_ref::<T>().unwrap().hash(h)
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    可惜

    fn my_hash<H: Hasher>(&self, hasher: &mut H);
    
    Run Code Online (Sandbox Code Playgroud)

    我猜是真的,但是后来我被困住了。(到目前为止,这似乎还是很荒谬的)。

能做到吗?如果是这样,怎么办?

我之前曾询问过PartialEq特质对象,这很困难,因为需要信息来提供特质对象的具体类型。这是通过向下转换解决的,但是我没有设法在此处应用该解决方案。

jfe*_*ard 8

我不是 Rust 专家,但在我看来,您试图将 Rust 变成 Java(不要生气:我真的很喜欢 Java)。

如何创建可散列的特征对象?

您不想创建 trait 对象的哈希表(这很容易),您想要创建一个不是trait 对象的 trait哈希表,这就是您遇到困难的原因。

问题

我总结:你必须实现某些特质各种结构MyTraitHash并且Eq,你想将这些混合结构成一个单一的一个hashstable为TunedMyTrait特征的对象。这需要 TunedMyTraitHashand 的子特征Eq。但是,虽然MyTrait可以成为 trait 对象,TunedMyTrait但不能

我相信你知道为什么,但我会尝试使用这个宝贵的资源,让其他读者明白这一点。(我用我自己的话来说,如果您认为不清楚,请不要害羞并进行编辑。)特征对象依赖于称为“对象安全”的东西(请参阅RFC 255)。“对象安全”意味着:特性的所有方法都必须是对象安全的。

Rust 对堆栈进行了大量使用,因此它必须知道它所能知道的一切的大小。在借用检查器之后,这是 Rust 的困难和优点之一。trait 对象有类型和大小:它是某种“胖”指针,包含有关具体类型的信息。每个方法调用都使用 a vtableof 方法委托给具体类型。我没有详细介绍,但是这个委托可能会出现一些问题,并且创建了“安全检查”来避免这些问题。这里:

  • 方法fn eq(&self, other: &Rhs) -> boolwhereRhs = Self不是对象安全的,因为在运行时Rhs被擦除,因此具体类型和大小other未知。
  • 该方法fn hash<H: Hasher>(&self, hasher: &mut H)不是对象安全的,因为vtable不是为每个具体类型构建的H

解决方案

好的。MyTrait是一个 trait 对象,但TunedMyTrait不是。然而,只有TunedMyTrait对象可能是哈希表的有效键。你能做什么?

您可以像以前一样尝试破解对象安全机制。您找到了 hack 的解决方案PartialEq(通过强制转换,如何测试 trait 对象之间的相等性?),并且您现在有另一个来自 @Boiethios 的 hack(它基本上使 hash 成为一个非通用函数)。如果你最终实现了目标,我可以想象代码的未来读者:“天哪,这家伙想做什么?” 或(更糟):“我不确定它的作用,但我很确定如果……它会运行得更快”。你已经破坏了语言的保护,你的代码很可能会产生比你试图解决的问题更糟糕的问题。这让我想起了这样的讨论:在运行时获取泛型类型的类。进而?你会用这段代码做什么?

或者你可以讲道理。有一些可能性:您使用具有真正相同具体类型的键的哈希表,将MyTrait对象装箱,使用枚举......可能还有其他方法(如上所述,我不是 Rust 专家)。

不要误会我的意思:破解一种语言真的很有趣,有助于深入了解它的机制和限制(注意:如果你没有问这个问题,我就不会仔细研究 DST 和 trait 对象,因此我谢谢你)。但是如果你打算做一些严肃的事情,你必须是认真的:Rust 不是 Java...

编辑

我想比较和散列运行时多态的对象。

这并不难,但你也想把它们放在一个 中HashMap,这就是问题所在。

我会给你另一个见解。基本上,您知道哈希表是一个桶数组。Rust 使用开放寻址来解决哈希冲突(特别是:罗宾汉哈希),这意味着每个桶将包含 0 或 1 pair (key, value)。当您将一对(key, value)放入空桶中时,元组(key, value)pair_start + index * sizeof::<K, V>()根据的定义offset写入缓冲区数组中的位置。很明显,你需要大小对。

如果您可以使用 trait 对象,您将拥有一个大小合适的胖指针。但由于已经说明的原因,这是不可能的。我提出的所有想法都集中在这一点上:有大小的键(假设值已经大小了)。混凝土类型:明显尺寸。装箱:指针的大小。枚举:最大元素的大小 + 标签的大小 + 填充。

拳击的基本例子

警告:我努力在互联网上找到一个例子,但没有找到任何东西。所以我决定从头开始创建一个基本的拳击示例,但我不确定这是正确的方法。如果需要,请评论或编辑。

首先,向您的 trait 添加一个方法,该方法标识MyTrait使用可比较和可散列值实现的任何具体类型的每个实例,假设有一个id返回 的方法i64

trait MyTrait {
    fn id(&self) -> i64; // any comparable and hashable type works instead of i64
}
Run Code Online (Sandbox Code Playgroud)

FooBar具体的类型将实现这个方法(这里给出的实现完全是愚蠢的):

struct Foo(u32);

impl MyTrait for Foo {
    fn id(&self) -> i64 {
        -(self.0 as i64)-1 // negative to avoid collisions with Bar
    }
}

struct Bar(String);

impl MyTrait for Bar {
    fn id(&self) -> i64 {
        self.0.len() as i64 // positive to avoid collisions with Foo
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,我们必须实现HashEq,要想把MyTrait一个HashMap。但是如果我们为 这样做MyTrait,我们会得到一个不能作为 trait 对象的 trait,因为MyTrait它没有大小。让我们为 实现它Box<Trait>,它的大小是:

impl Hash for Box<MyTrait> {
    fn hash<H>(&self, state: &mut H) where H: Hasher {
        self.id().hash(state)
    }
}

impl PartialEq for Box<MyTrait> {
    fn eq(&self, other: &Box<MyTrait>) -> bool {
        self.id() == other.id()
    }
}

impl Eq for Box<MyTrait> {}
Run Code Online (Sandbox Code Playgroud)

我们使用id方法来实现eqhash

现在,想一想Box<MyTrait>: 1. 它的大小;2. 它实现HashEq。这意味着它可以用作 a 的键HashMap

fn main() {
    let foo = Foo(42);
    let bar = Bar("answer".into());
    let mut my_map = HashMap::<Box<MyTrait>, i32>::new();
    my_map.insert(Box::new(foo), 1);
    my_map.insert(Box::new(bar), 2);

    println!("{:?}", my_map.get(&(Box::new(Foo(42)) as Box<MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Foo(41)) as Box<MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Bar("answer".into())) as Box<MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Bar("question".into())) as Box<MyTrait>)));
Run Code Online (Sandbox Code Playgroud)

}

输出:

    Some(1)
    None
    Some(2)
    None
Run Code Online (Sandbox Code Playgroud)

试试看:https : //play.integer32.com/?gist=85edc6a92dd50bfacf2775c24359cd38&version=stable

我不确定它是否能解决您的问题,但我真的不知道您要做什么...