[英]How can I create hashable trait objects / trait objects with generic method parameters?
我有一些实现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
是方式,我需要一个可以转换为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 } }
然后垂头丧气
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
我猜这是真的,但后来我被困住了。 (到目前为止,这似乎有点荒谬)。
这可以做到吗? 如果是这样,怎么做?
我之前问过关于 trait objects 的PartialEq
,这很难,因为需要 trait 对象的具体类型信息。 这是通过向下转换解决的,但我没有设法在此处应用该解决方案。
我不是 Rust 专家,但在我看来你试图将 Rust 变成 Java(不要生气:我真的很喜欢 Java)。
如何创建可散列的特征对象?
您不想创建特征对象的哈希表(这很容易),您想创建不是特征对象的特征哈希表,这就是您遇到困难的原因。
我总结一下:您有一些实现特征MyTrait
、 Hash
和Eq
的各种结构,并且您希望将这些混合结构作为TunedMyTrait
特征对象放入单个哈希表中。 这要求TunedMyTrait
是Hash
和Eq
的子特征。 但是,虽然MyTrait
可以成为特征对象, TunedMyTrait
不能。
我相信您知道原因,但我会尝试使用这个宝贵的资源让其他读者明白这一点。 (我用我自己的话来说,如果您认为不清楚,请不要害羞并对其进行编辑。)特征对象依赖于称为“对象安全”的东西(请参阅RFC 255 )。 “对象安全”意味着:特征的所有方法都必须是对象安全的。
Rust 大量使用堆栈,因此它必须知道它所能知道的一切的大小。 在借用检查器之后,这就是 Rust 的困难和优点之一。 特征对象是有类型和大小的:它是某种“胖”指针,包含有关具体类型的信息。 每个方法调用都委托给具体类型,使用方法的vtable
。 我没有详细说明,但是这个委托可能会出现一些问题,并且创建了“安全检查”来避免这些问题。 这里:
fn eq(&self, other: &Rhs) -> bool
where Rhs = Self
不是对象安全的,因为在运行时, Rhs
被擦除,因此other
的具体类型和大小是未知的。fn hash<H: Hasher>(&self, hasher: &mut H)
不是对象安全的,因为vtable
不是为每个具体类型H
构建的。好的。 MyTrait
是一个特征对象,但TunedMyTrait
不是。 然而,只有TunedMyTrait
对象可能是哈希表的有效键。 你能做什么?
您可以像以前一样尝试破解对象安全机制。 您找到了破解PartialEq
的解决方案(通过强制转换尝试, 如何测试 trait 对象之间的相等性? ),并且您现在有了来自 @Boiethios 的另一个 hack(它基本上使 hash 成为非通用函数)。 如果你最终达到目标,我可以想象代码的未来读者:“天哪,这家伙想做什么?” 或者(更糟糕的):“我不确定它的作用,但我很确定如果......它会运行得更快”。 你已经破解了语言的保护,你的代码可能会产生比你试图解决的问题更糟糕的问题。 这让我想起了这样的讨论: Get generic type of class at runtime 。 接着? 你将如何处理这段代码?
或者你可以讲道理。 有一些可能性:您使用具有相同具体类型的键的哈希表,将MyTrait
对象装箱,使用枚举...可能还有其他方法(如前所述,我不是 Rust 专家) .
不要误会我的意思:破解一门语言真的很有趣,有助于深入理解它的机制和限制(注意:如果你没有问过这个问题,我不会仔细研究 DST 和 trait 对象,因此我谢谢你)。 但是如果你打算做一些严肃的事情,你必须认真:Rust 不是 Java...
编辑
我想比较和散列运行时多态的对象。
这并不难,但您还想将它们放在HashMap
中,这就是问题所在。
我会给你另一个见解。 基本上,您知道哈希表是一个桶数组。 Rust 使用开放寻址来解决哈希冲突(特别是:罗宾汉哈希),这意味着每个桶将包含 0 或 1 对(key, value)
。 (key, value)
(key, value)
pair_start + index * sizeof::<K, V>()
_ offset
的定义。 很明显,您需要大小合适的配对。
如果您可以使用 trait 对象,您将拥有大小合适的胖指针。 但由于已经说明的原因,这是不可能的。 我提出的所有想法都集中在这一点上:有大小的键(假设值已经确定了大小)。 混凝土类型:明显尺寸。 装箱:指针的大小。 枚举:最大元素的大小+标签的大小+填充。
警告:我试图在互联网上找到一个例子,但没有找到任何东西。 所以我决定从头开始创建一个拳击的基本示例,但我不确定这是不是正确的方法。 如果需要,请发表评论或编辑。
首先,向您的 trait 添加一个方法,该方法标识实现MyTrait
具有可比较和可哈希值的任何具体类型的每个实例,比方说一个返回i64
的id
方法:
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
这样做,我们会得到一个不能成为 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> {}
我们使用id
方法来实现eq
和hash
。
现在,想想Box<MyTrait>
: 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
我不确定它是否能解决您的问题,但我真的不知道您要做什么...
您可以将所需的功能放入您的 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 旁边的具体类型是什么。 有人可以为两种相同类型实现特征,例如:
struct Foo(String);
struct Bar(String);
在这种情况下,你想如何处理Foo("hello")
vs 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);
}
}
特质只是对事物的约定或部分考虑(就像您说“人类”是“开发者”时一样)。 您不应该(以我的拙见)将特征视为具体类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.