简体   繁体   English

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

[英]How can I create hashable trait objects / trait objects with generic method parameters?

I have some structs that implement both Hash and MyTrait .我有一些实现HashMyTrait的结构。 I'm using them as &MyTrait trait objects.我将它们用作&MyTrait特征对象。

Now I want &MyTrait to also implement Hash .现在我希望&MyTrait也实现Hash I've tried a few things:我尝试了几件事:

  • Naively, trait MyTrait: Hash {} :天真地, trait MyTrait: Hash {}

     the trait `MyTrait` cannot be made into an object
  • Then I tried this:然后我尝试了这个:

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

    but I need to delegate to the hash method of the concrete type of self , I think.但我认为,我需要委托给self的具体类型的hash方法。

  • So the naive next step is to put this on MyTrait :所以天真的下一步就是把它放在MyTrait上:

     fn my_hash<H: Hasher>(&self, hasher: &mut H);

    which brings me right back to the first point.这让我回到了第一点。

  • I read something about using a trait object instead of generic parameter, which sounds smart, so I put this on MyTrait我读了一些关于使用 trait 对象而不是泛型参数的内容,这听起来很聪明,所以我把它放在MyTrait

     fn my_hash(&self, hasher: &mut H);

    Then I need to actually implement this.然后我需要实际实现这一点。 Preferably not by hand for every trait:最好不要手动处理每个特征:

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

    but then但是之后

    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

    So I'd have to downcast Hasher所以我不得不对Hasher ……

  • If downcasting Hasher is the way, I need a generic parameter H that can convert to an Any Hasher , Let's try:如果向下转换Hasher是方式,我需要一个可以转换为Any Hasher的通用参数H ,让我们尝试:

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

    and then to downcast然后垂头丧气

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

    but alas可惜

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

    which I guess is true, but then I'm stuck.我猜这是真的,但后来我被困住了。 (Also it seems kind of ridiculous so far). (到目前为止,这似乎有点荒谬)。

Can this can be done?这可以做到吗? If so, how?如果是这样,怎么做?

I previously asked about PartialEq for trait objects , which was hard because information the concrete type of the trait object is needed.我之前问过关于 trait objects 的PartialEq ,这很难,因为需要 trait 对象的具体类型信息。 That was solved with downcasting, but I didn't manage to apply that solution here.这是通过向下转换解决的,但我没有设法在此处应用该解决方案。

I'm not a Rust expert, but it seems to me that you try to turn Rust into Java (don't be offended: I really do like Java).我不是 Rust 专家,但在我看来你试图将 Rust 变成 Java(不要生气:我真的很喜欢 Java)。

How can I create hashable trait objects?如何创建可散列的特征对象?

You don't want to create a hashtable of trait objects (it's easy), you want to create a hashtable of traits that are not trait objects and that's why you encounter difficulties.您不想创建特征对象的哈希表(这很容易),您想创建不是特征对象的特征哈希表,这就是您遇到困难的原因。

The problem问题

I summarize: you have some various structs that implement traits MyTrait , Hash and Eq , and you would like to put those mixed structs into a single a hashtable as a TunedMyTrait trait objects.我总结一下:您有一些实现特征MyTraitHashEq的各种结构,并且您希望将这些混合结构作为TunedMyTrait特征对象放入单个哈希表中。 This requires TunedMyTrait to be a subtrait of Hash and Eq .这要求TunedMyTraitHashEq的子特征。 But whereas MyTrait can be made a trait object, TunedMyTrait cannot .但是,虽然MyTrait可以成为特征对象, TunedMyTrait不能

I'm sure you know why, but I will try to make it clear for other readers, using this valuable resource .我相信您知道原因,但我会尝试使用这个宝贵的资源让其他读者明白这一点。 (I put it in my own words, don't be shy and edit it if you think that isn't clear.) Trait objects rely on something that is called "object safety" (see the RFC 255 ). (我用我自己的话来说,如果您认为不清楚,请不要害羞并对其进行编辑。)特征对象依赖于称为“对象安全”的东西(请参阅RFC 255 )。 "Object safety" means: all methods of the trait must be object-safe. “对象安全”意味着:特征的所有方法都必须是对象安全的。

Rust makes an intensive usage of the stack, thus it has to know the size of everything it can. Rust 大量使用堆栈,因此它必须知道它所能知道的一切的大小。 After the borrow checker, that's one of the difficulties and the beauties of Rust.在借用检查器之后,这就是 Rust 的困难和优点之一。 A trait object is typed and sized: it is some kind of "fat" pointer that contains information on the concrete type.特征对象是有类型和大小的:它是某种“胖”指针,包含有关具体类型的信息。 Every method call is delegated to the concrete type, using a vtable of methods.每个方法调用都委托给具体类型,使用方法的vtable I don't get into details, but some issues may occur with this delegation and the "safety check" was created to avoid those issues.我没有详细说明,但是这个委托可能会出现一些问题,并且创建了“安全检查”来避免这些问题。 Here:这里:

  • the method fn eq(&self, other: &Rhs) -> bool where Rhs = Self is not object safe, because at runtime, Rhs was erased, and thus the concrete type and the size of other is not known.方法fn eq(&self, other: &Rhs) -> bool where Rhs = Self不是对象安全的,因为在运行时, Rhs被擦除,因此other的具体类型和大小是未知的。
  • the method fn hash<H: Hasher>(&self, hasher: &mut H) is not object safe, because the vtable is not built for every concrete type H .方法fn hash<H: Hasher>(&self, hasher: &mut H)不是对象安全的,因为vtable不是为每个具体类型H构建的。

The solution解决方案

Okay.好的。 MyTrait is a trait object, but TunedMyTrait is not. MyTrait是一个特征对象,但TunedMyTrait不是。 Yet only TunedMyTrait objects may be valid keys for your hashtable.然而,只有TunedMyTrait对象可能是哈希表的有效键。 What can you do?你能做什么?

You can try, as you did, to hack the object safety mechanism.您可以像以前一样尝试破解对象安全机制。 You found a solution to hack PartialEq (with a cast try, How to test for equality between trait objects? ), and you have now another hack from @Boiethios (which basically makes of hash a non generic function).您找到了破解PartialEq的解决方案(通过强制转换尝试, 如何测试 trait 对象之间的相等性? ),并且您现在有了来自 @Boiethios 的另一个 hack(它基本上使 hash 成为非通用函数)。 If you finally reach your goal, I can imagine the future reader of the code: "OMG, what is this guy trying to do?"如果你最终达到目标,我可以想象代码的未来读者:“天哪,这家伙想做什么?” or (worse): "I'm not sure of what it does, but I'm pretty sure it would run faster if...".或者(更糟糕的):“我不确定它的作用,但我很确定如果......它会运行得更快”。 You have hacked a protection of the language and your code is likely to create issues worse than the problem you are trying to solve.你已经破解了语言的保护,你的代码可能会产生比你试图解决的问题更糟糕的问题。 This reminds me this kind of discussion: Get generic type of class at runtime .这让我想起了这样的讨论: Get generic type of class at runtime And then?接着? What will you do with this piece of code?你将如何处理这段代码?

Or you can be reasonable.或者你可以讲道理。 There are some possiblities: you use a hashtable with keys that are really of the same concrete type, you box your MyTrait objects, you use an enum... There may be other ways (as said, I'm not a Rust expert).有一些可能性:您使用具有相同具体类型的键的哈希表,将MyTrait对象装箱,使用枚举...可能还有其他方法(如前所述,我不是 Rust 专家) .

Don't get me wrong: hacking a language is really fun and helps to understand deeply its mechanics and limits (note: if you hadn't asked that question, I wouldn't have had a close look at DST and trait objects, thus I thank you).不要误会我的意思:破解一门语言真的很有趣,有助于深入理解它的机制和限制(注意:如果你没有问过这个问题,我不会仔细研究 DST 和 trait 对象,因此我谢谢你)。 But you have to be serious if you intend to do something serious: Rust is not Java...但是如果你打算做一些严肃的事情,你必须认真:Rust 不是 Java...

EDIT编辑

I want to compare and hash objects that are runtime-polymorphic.我想比较和散列运行时多态的对象。

That's not difficult, but you also want to put them in a HashMap , and that is the problem.这并不难,但您还想将它们放在HashMap中,这就是问题所在。

I will give you another insight.我会给你另一个见解。 Basically, you know that a hashtable is an array of buckets.基本上,您知道哈希表是一个桶数组。 Rust uses open adressing to resolve hash collisions (specifically: Robin Hood hashing), that means that every bucket will contain 0 or 1 pair (key, value) . Rust 使用开放寻址来解决哈希冲突(特别是:罗宾汉哈希),这意味着每个桶将包含 0 或 1 对(key, value) When you put a pair (key, value) in an empty bucket , the tuple (key, value) is written in the buffer array, at the position pair_start + index * sizeof::<K, V>() , according to the definition of offset . (key, value) (key, value) pair_start + index * sizeof::<K, V>() _ offset的定义 It's obvious that you need sized pairs.很明显,您需要大小合适的配对。

If you could use trait object, you would have fat pointer, which is sized.如果您可以使用 trait 对象,您将拥有大小合适的胖指针。 But that's not possible for the reasons already stated.但由于已经说明的原因,这是不可能的。 All the ideas I proposed are focused on this: have sized keys (assuming that values are already sized).我提出的所有想法都集中在这一点上:有大小的键(假设值已经确定了大小)。 Concrete type: obviously sized.混凝土类型:明显尺寸。 Boxing: size of a pointer.装箱:指针的大小。 Enum: size of the biggest element + size of the tag + padding.枚举:最大元素的大小+标签的大小+填充。

Basic example with boxing拳击的基本例子

WARNING: I tried hard to find an example on the internet, but didn't find anything.警告:我试图在互联网上找到一个例子,但没有找到任何东西。 So I decided to create from scratch a basic example with boxing, but I'm not sure this is the right way to do it.所以我决定从头开始创建一个拳击的基本示例,但我不确定这是不是正确的方法。 Please comment or edit if needed.如果需要,请发表评论或编辑。

First, add to your trait a method that identifies every instance of any concrete type that implements MyTrait with a comparable and hashable value, let's say an id method that returns an i64 :首先,向您的 trait 添加一个方法,该方法标识实现MyTrait具有可比较和可哈希值的任何具体类型的每个实例,比方说一个返回i64id方法:

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

Foo and Bar concrete types will implement this method (the implementation given here is totally stupid): 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
    }
}

Now, we have to implement Hash and Eq , in order to put MyTrait in a HashMap .现在,我们必须实现HashEq ,以便将MyTrait放入HashMap中。 But if we do it for MyTrait , we get a trait that can't be a trait object, because MyTrait is not sized.但是如果我们为MyTrait这样做,我们会得到一个不能成为 trait 对象的 trait,因为MyTrait没有大小。 Let's implement it for Box<Trait> , which is sized:让我们为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> {}

We used the id method to implement eq and hash .我们使用id方法来实现eqhash

Now, think of Box<MyTrait> : 1. it is sized;现在,想想Box<MyTrait> : 1. 它的大小; 2. it implements Hash and Eq . 2. 它实现了HashEq That means that it can be used as a key for a HashMap :这意味着它可以用作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>)));

} }

Output:输出:

    Some(1)
    None
    Some(2)
    None

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

I'm not sure it solves your problem, but I don't really know what you are trying to do...我不确定它是否能解决您的问题,但我真的不知道您要做什么...

You can put the desired functionalities in your trait, ie you mix your second and third attempts:您可以将所需的功能放入您的 trait 中,混合您的第二次和第三次尝试:

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 Hasher);
    fn my_eq(&self, other: &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 Hasher) {
        self.hash(&mut h)
    }
}

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

// Implement needed traits for your trait

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

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

impl Eq for MyTrait {}

// This compiles

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

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

In my opinion, this is not a good thing to implement Hash for a trait in your way, because you do not know what is the concrete type beside the trait.在我看来,以你的方式为 trait 实现Hash并不是一件好事,因为你不知道 trait 旁边的具体类型是什么。 Someone could implement the trait for both the same type, like:有人可以为两种相同类型实现特征,例如:

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

In this case, how do you want to handle Foo("hello") vs Bar("hello") ?在这种情况下,你想如何处理Foo("hello") vs Bar("hello") Are they the same item?它们是同一个项目吗? Because they will have the same hash.因为它们将具有相同的哈希值。

The real question, in your case, is: how do you define what is same or not from a trait?就您而言,真正的问题是:您如何定义特征的相同或不同? In my opinion, a better way to handle this is to calculate the hash from "business" trait methods, for example:在我看来,更好的处理方法是从“业务”特征方法计算哈希,例如:

#[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 MyTrait {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        self.description().hash(hasher);
        self.get_baz().hash(hasher);
        self.get_qux().hash(hasher);
    }
}

A trait is only a contract or a partial consideration of a thing (just like when you say that a "human being" is a "developer").特质只是对事物的约定或部分考虑(就像您说“人类”是“开发者”时一样)。 You should not (in my humble opinion) consider a trait as a concrete type.您不应该(以我的拙见)将特征视为具体类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 通用特征对象的向量 - Vector of generic trait objects 在特征对象中使用泛型类型参数会引起什么问题? - What is the cited problem with using generic type parameters in trait objects? 如何在 Rust 中没有特征对象的特征的多个实现者之间进行选择? - How can I choose between multiple implementors of a trait without trait objects in Rust? 如何在Scala上使用Trait创建类? - How can I create A Class with Trait On Scala? 如何使用可以是多种特征对象的参数定义函数? - How can I define a function with a parameter that can be multiple kinds of trait objects? 如何在另一个泛型类型的特征绑定的类型参数上表达特征? - How can I express a trait bound on a type parameter for another generic type's trait bound? trait 对象如何将带有泛型方法的 trait 作为参数? - How can a trait object take a trait with generic methods as an argument? 用返回通用特征的方法实现特征 - Implementing trait with method returning generic trait “特征函数的通用参数”和“特征的通用参数”有什么区别? - What is the difference between “generic parameters of trait function” and “generic parameters of trait”? 当存在单个对象的转换方法时,如何创建通用方法来转换对象列表? - How can I create a generic method to convert lists of objects when conversion methods for single objects exist?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM