rust 如何创建可散列的trait对象/带有泛型方法参数的trait对象?

syqv5f0l  于 2023-11-19  发布在  其他
关注(0)|答案(2)|浏览(114)

我有一些同时实现HashMyTrait的结构体,我把它们用作&dyn MyTrait trait对象。
现在我想让&dyn MyTrait也实现Hash。我已经尝试了几件事:

  • 简单地说,trait MyTrait: Hash {}
the trait `MyTrait` cannot be made into an object

字符串

  • 然后我试着这样做:
impl Hash for dyn MyTrait {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        // ...
    }
}


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

  • 因此,简单的下一步是将其放在MyTrait上:
fn my_hash<H: Hasher>(&self, hasher: &mut H);


这让我回到第一点。

  • 我读到了一些关于使用trait对象而不是泛型参数的东西,这听起来很聪明,所以我把它放在MyTrait
fn my_hash(&self, hasher: &mut H);


然后我需要实际实现它。最好不要对每个特征都手工实现:

impl<T: 'static + Hash> MyTrait for T {
    fn as_any(&self) -> &dyn Any {
        self as &dyn Any
    }

    fn my_hash(&self, hasher: &mut Hasher) {
        self.as_any().downcast_ref::<T>().unwrap().hash(hasher)
    }
}


但后来

the trait bound `std::hash::Hasher: std::marker::Sized` is not satisfied
`std::hash::Hasher` does not have a constant size known at compile-time


所以我必须向下转换Hasher ...

  • 如果向下转换Hasher的方式,我需要一个通用参数H,可以转换为AnyHasher,让我们试试:
trait AnyHasher {
    fn as_any(&self) -> &dyn Any;
}

impl<H: 'static + Hasher> AnyHasher for H {
    fn as_any(&self) -> &dyn Any {
        self as &dyn Any
    }
}


然后沮丧地

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)
    }
}


但唉

the type parameter `H` is not constrained by the impl trait, self type, or predicates


我想这是真的,但我被卡住了。(到目前为止,这似乎有点荒谬)。
这可以做到吗?如果可以,如何做到?
我之前问过PartialEq for trait objects,这很难,因为需要trait对象的具体类型的信息。这是通过向下转换解决的,但我没有设法在这里应用该解决方案。

irtuqstp

irtuqstp1#

我不是RustMaven,但在我看来,你试图把Rust变成Java(不要生气:我真的很喜欢Java)。
如何创建可散列的trait对象?
你不想创建一个trait对象的哈希表(这很容易),你想创建一个trait对象的哈希表,这就是为什么你遇到困难。

问题

我总结如下:你有一些实现trait MyTraitHashEq的不同结构体,你想把这些混合结构体作为TunedMyTrait trait对象放入一个哈希表中。这需要TunedMyTraitHashEq的subtrait。但是MyTrait可以成为trait对象,* TunedMyTrait不能 *。
我相信你知道为什么,但是我会尝试用this valuable resource来向其他读者解释清楚。(我用自己的话写的,如果你觉得不清楚,不要害羞,编辑它。)Trait对象依赖于一种叫做“对象安全”的东西(参见the RFC 255)。“对象安全”意味着:trait的所有方法都必须是对象安全的。
Rust大量使用堆栈,因此它必须知道它所能知道的所有东西的大小。在借用检查器之后,这是Rust的难点之一,也是Rust的优点之一。一个trait对象是有类型和大小的:它是某种“胖”指针,包含关于具体类型的信息。每个方法调用都委托给具体类型,使用vtable的方法。我不会详细介绍,但这种委托可能会发生一些问题,并且创建了“安全检查”来避免这些问题。这里:

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

解决方案

好的。MyTrait是一个trait对象,但TunedMyTrait不是。然而只有TunedMyTrait对象可能是哈希表的有效键。你能做什么?
您可以尝试,就像您所做的那样,破解对象安全机制。(有一个铸造尝试,How to test for equality between trait objects?),你现在有另一个黑客从@Boiethios如果你最终达到了你的目标,我可以想象代码的未来读者:“OMG,这家伙想做什么?”或者(更糟):“I'm not sure of what it does,but I'm pretty sure it would run faster if.
或者你也可以理性一点。有一些可能性:你使用一个哈希表,它的键实际上是相同的具体类型,你把你的MyTrait对象装箱,你使用一个枚举..可能还有其他的方法(正如我所说的,我不是RustMaven)。
别误会我的意思破解一门语言真的很有趣,有助于深入理解它的机制和限制(注意:如果你没有问这个问题,我就不会仔细研究DST和trait对象,因此我感谢你)。但是如果你打算做一些严肃的事情,你必须严肃:Rust不是Java。

编辑

我想比较和散列运行时多态的对象。
这并不难,但你也想把它们放在HashMap中,这就是问题所在。
我会给予你另一个见解。基本上,你知道哈希表是一个桶数组。Rust使用开放寻址来解决哈希冲突。(具体而言:罗宾汉散列),这意味着每个桶将包含0或1对(key, value)。当您将一对(key, value)放入空桶时,元组(key, value)写入缓冲区数组,在位置pair_start + index * sizeof::<K, V>(),根据the definition of offset。很明显,你需要 sized 对。
如果你可以使用trait对象,你会有一个胖指针,它是有大小的。但这是不可能的,原因已经说过了。我提出的所有想法都集中在这一点上:有大小的键(假设值已经被大小化了)。具体类型:明显的大小。装箱:指针的大小。枚举:最大元素的大小+标签的大小+填充。

装箱基本示例

联系我们我努力在互联网上找到一个例子,但没有找到任何东西。所以我决定从头开始创建一个基本的装箱示例,但我不确定这是正确的方法。如果需要,请评论或编辑。*
首先,在trait中添加一个方法,该方法标识实现MyTrait的任何具体类型的每个示例,并具有comparablehashable值,假设一个id方法返回i64

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

字符串
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
    }
}

现在,我们必须实现HashEq,以便将dyn MyTrait放入HashMap中。但是如果我们为dyn MyTrait这样做,我们得到的trait不可能是trait对象,因为dyn MyTrait没有大小。让我们为Box<dyn Trait>实现它,它是大小:

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

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

impl Eq for Box<dyn MyTrait> {}


我们使用id方法来实现eqhash
现在,想想Box<dyn MyTrait>:1.它的大小; 2.它实现了HashEq。这意味着它可以用作HashMap的密钥:

fn main() {
    let foo = Foo(42);
    let bar = Bar("answer".into());
    let mut my_map = HashMap::<Box<dyn 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<dyn MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Foo(41)) as Box<dyn MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Bar("answer".into())) as Box<dyn MyTrait>)));
    println!("{:?}", my_map.get(&(Box::new(Bar("question".into())) as Box<dyn MyTrait>)));
}


输出量:

Some(1)
None
Some(2)
None


试试看:https://play.integer32.com/?gist=85edc6a92dd50bfacf2775c24359cd38&version=stable
我不确定它能解决你的问题,但我真的不知道你想做什么...

8dtrkrch

8dtrkrch2#

你可以把你想要的功能放在你的特质中,* 也就是说,* 你可以混合你的第二次和第三次尝试:

use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;

#[derive(Hash)]
struct Foo(i32);

#[derive(Hash)]
struct Bar(String);

// Put the desired functionalities in your trait

trait MyTrait {
    fn my_hash(&self, h: &mut dyn Hasher);
    fn my_eq(&self, other: &dyn MyTrait) -> bool {
        let mut hasher1 = DefaultHasher::new();
        let mut hasher2 = DefaultHasher::new();

        self.my_hash(&mut hasher1);
        other.my_hash(&mut hasher2);
        hasher1.finish() == hasher2.finish()
    }

    // other funcs
}

impl MyTrait for Foo {
    fn my_hash(&self, mut h: &mut dyn Hasher) {
        self.hash(&mut h);
    }
}

impl MyTrait for Bar {
    fn my_hash(&self, mut h: &mut dyn Hasher) {
        self.hash(&mut h);
    }
}

// Implement needed traits for your trait

impl Hash for dyn MyTrait {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        self.my_hash(hasher);
    }
}

impl PartialEq for dyn MyTrait {
    fn eq(&self, other: &dyn MyTrait) -> bool {
        self.my_eq(other)
    }
}

impl Eq for dyn MyTrait {}

// This compiles

fn main() {
    let foo = Foo(42);
    let bar = Bar("answer".into());
    let mut set = HashSet::new();

    set.insert(&foo as &dyn MyTrait);
    set.insert(&bar);
}

字符串
在我看来,以你的方式为trait实现Hash并不是一件好事,因为你不知道trait旁边的具体类型是什么。有人可以为同一类型实现trait,比如:

struct Foo(String);
struct Bar(String);


在这种情况下,你想如何处理Foo("hello")Bar("hello")?它们是同一个项目吗?因为它们将具有相同的哈希值。
在你的例子中,真实的问题是:* 你如何定义trait中的相同或不同?* 在我看来,处理这个问题的更好方法是从“业务”trait方法中计算hash,例如:

#[derive(Hash)]
struct Baz(...); // Business item
#[derive(Hash)]
struct Qux(...); // Another business item

trait MyTrait {
    // all those returned items make my MyTrait unique
    fn description(&self) -> &str;
    fn get_baz(&self) -> Baz;
    fn get_qux(&self) -> Qux;
}

impl Hash for dyn MyTrait {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        self.description().hash(hasher);
        self.get_baz().hash(hasher);
        self.get_qux().hash(hasher);
    }
}


一个特质只是一个契约或一个事物的部分考虑(就像当你说一个“人”是一个“开发者”)。你不应该(在我的拙见中)把一个特质看作一个具体的类型。

相关问题