【问题标题】:How can I create hashable trait objects / trait objects with generic method parameters?如何使用泛型方法参数创建可散列的特征对象/特征对象?
【发布时间】:2018-04-07 20:03:13
【问题描述】:

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

现在我希望 &MyTrait 也实现 Hash。我尝试了一些方法:

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

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

    impl Hash for 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) -> &Any {
            self as &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 可以转换为Any Hasher,让我们试试:

    trait AnyHasher {
        fn as_any(&self) -> &Any;
    }
    
    impl<H: 'static + Hasher> AnyHasher for H {
        fn as_any(&self) -> &Any {
            self as &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,这很难,因为需要特征对象的具体类型信息。这是通过向下转换解决的,但我没有设法在此处应用该解决方案。

【问题讨论】:

  • 你的第一个例子works for me
  • @Jmb 是的,但不可能制作具有该特征的对象:play.integer32.com/…(或者我不知道如何)
  • 我遇到了和这个差不多的情况,你解决了吗?
  • 为什么不在第二次尝试调用的 trait 中添加 hash 方法?
  • @Mark 我正在写 POC

标签: generics rust traits


【解决方案1】:

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

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

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

问题

我总结一下:您有一些实现特征MyTrait、Hash 和Eq 的各种结构,并且您希望将这些混合结构作为TunedMyTrait 特征对象放入单个哈希表中。这需要 TunedMyTrait 是 Hash 和 Eq 的子特征。但是MyTrait 可以作为特征对象,TunedMyTrait 不能。

我相信您知道原因,但我会尝试使用this valuable resource 向其他读者说明这一点。 (我用我自己的话来说,如果您认为不清楚,请不要害羞并对其进行编辑。)特征对象依赖于称为“对象安全”的东西(请参阅the RFC 255)。 “对象安全”的意思是:特征的所有方法都必须是对象安全的。

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

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

解决办法

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

您可以尝试破解对象安全机制。您找到了破解 PartialEq 的解决方案(尝试使用强制转换,How to test for equality between trait objects?),并且您现在有了来自 @Boiethios 的另一个 hack(它基本上使 hash 成为非通用函数)。如果你最终达到目标,我可以想象代码的未来读者:“天哪,这家伙想做什么?”或者(更糟糕的):“我不确定它的作用,但我很确定如果......它会运行得更快”。你已经破解了语言的保护,你的代码可能会产生比你试图解决的问题更糟糕的问题。这让我想起了这种讨论:Get generic type of class at runtime。进而?你将如何处理这段代码?

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

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

编辑

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

这并不难,但您还想将它们放在HashMap 中,这就是问题所在。

我会给你另一个见解。基本上,您知道哈希表是一个桶数组。 Rust 使用开放寻址来解决哈希冲突(特别是:罗宾汉哈希),这意味着每个桶将包含 0 或 1 对 (key, value)。当你put a pair (key, value) in an empty bucket 时,元组(key, value) 被写入缓冲区数组中,位置为pair_start + index * sizeof::&lt;K, V&gt;(),根据the definition of offset。很明显,您需要 sized 对。

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

拳击的基本示例

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

首先,向您的 trait 添加一个方法,该方法可以识别实现 MyTrait 的任何具体类型的每个实例,并带有 comparable 和 hashable 值,比如说 @987654359返回i64的@方法:

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

Foo 和Bar 具体类型会实现这个方法(这里给出的实现是完全愚蠢的):

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

现在,我们必须实现Hash 和Eq,以便将MyTrait 放入HashMap。但是如果我们为MyTrait 这样做,我们会得到一个不能作为特征对象的特征,因为MyTrait 没有大小。让我们为Box&lt;Trait&gt; 实现它,它的大小是:

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

我们使用id 方法来实现eq 和hash。

现在,想想Box&lt;MyTrait&gt;: 1. 它的大小; 2.它实现了Hash和Eq。这意味着它可以用作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>)));

}

输出:

    Some(1)
    None
    Some(2)
    None

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

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

【讨论】:

  • 总结是对的,只不过@9​​87654382@和MyTrait的区别是实现的东西;如果它们可以相同,那就更好了。至于 Java,是的,它可能基于在 jit 或运行时构建 vtable 的各种语言。但我认为这个问题是合理的(解决方案可能不是):我想比较和散列运行时多态的对象。特征对象不是唯一的运行时多态性吗?令我惊讶的是,这个问题并没有更频繁地出现。
  • 枚举的想法很有创意,我喜欢。但它受到更多限制,它只适用于封闭的对象组。
  • id也是一个创造性的解决方案,谢谢。当然,完全唯一的 id 比散列更难,所以它可以解决问题。但在适用的情况下,这种方式的代码要简单得多。
  • 枚举示例:play.integer32.com/…
  • @Mark 枚举的例子真的很酷。关于独特的id:我的感觉是,如果你不能产生正确的id,那么对象不应该放在同一个HashMap或HashSet中。也就是说,如果您想要一个完全独特的id,请使用format!("{:p}", &amp;foo))(作为字符串的地址)之类的东西。但是如果一个Foo 可能等于另一个Foo,甚至等于一个Bar,你必须考虑特征的主要属性。在您的示例中,我将使用一个元组 (u64, String): (0, a.to_text()) 代表 Alphas 和 (1, b.to_text()) 代表 Betas。
【解决方案2】:

您可以将所需的功能放入您的 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);
}

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

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

在这种情况下,您想如何处理Foo("hello") 与Bar("hello")?它们是同一个项目吗?因为它们将具有相同的哈希值。

在您的情况下,真正的问题是:您如何定义特征的相同或不同?在我看来,处理此问题的更好方法是从 "业务”特征方法,例如:

#[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);
    }
}

特质只是对事物的契约或部分考虑(就像你说“人”是“开发者”一样)。您不应该(以我的拙见)将特征视为具体类型。

【讨论】:

  • 哦,是的,这很聪明,我觉得很愚蠢,因为我不考虑只返回结构的哈希值!然而……
  • 我真希望我可以使用相同的Hasher,稍微提高性能(这样我只需委托给结构的hash 方法),但主要是因为它看起来像@987654330 @ 被传递是有原因的(尽管我不太清楚 Hashers 的不同之处)。
  • (这就是我的意思:play.integer32.com/…) - 不过不要误会,答案很聪明,适用于大多数实际情况;如果没有更好的表现,我会接受它
  • @Mark:你的意思是跨多个对象重用同一个Hasher实例?
  • @MatthieuM。理想情况下,相同的实例,但相同的类型已经是一种改进。
猜你喜欢
  • 1970-01-01
  • 2022-10-05
  • 1970-01-01
  • 1970-01-01
  • 2019-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多