Rust中用Trait模拟Java接口存入HashSet时遇对象安全问题
Rust中Trait对象无法存入HashSet的解决方法
问题场景
尝试定义HasVoice trait模拟Java接口,让Duck和Dog结构体实现该trait,再将实例存入HashSet<Box<dyn HasVoice>>时编译报错。
原代码
use std::collections::HashSet; use std::hash::Hash; trait HasVoice: PartialEq + Eq + Hash { fn talk(self: &Self) {} } #[derive(PartialEq, Hash, Eq)] struct Duck { name: String, } #[derive(PartialEq, Hash, Eq)] struct Dog { breed: String, } impl HasVoice for Duck { fn talk(self: &Self) { println!("duck quack!") } } impl HasVoice for Dog { fn talk(self: &Self) { println!("dog bark!") } } fn test() { let duck: Duck = Duck { name: "duckie".to_string(), }; let dog: Dog = Dog { breed: "labrador".to_string(), }; let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new(); quack_set.insert(Box::new(duck)); quack_set.insert(Box::new(dog)); }
错误信息
the trait `HasVoice` cannot be made into an object `HasVoice` cannot be made into an objectrustcClick for full compiler diagnostic main.rs(59, 17): for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
错误原因
HasVoice继承了PartialEq、Eq、Hash trait,这些trait的核心方法(比如PartialEq::eq的签名是fn eq(&self, other: &Self) -> bool)依赖于Self类型参数。对于trait对象dyn HasVoice来说,Self是动态大小类型(DST),且运行时无法保证other的具体类型与self一致,不符合Rust的对象安全规则,因此无法将HasVoice转为trait对象。
解决方案
方案一:用枚举统一类型(推荐)
放弃trait对象,用枚举封装所有需要的类型,枚举可自动派生PartialEq、Eq、Hash,完美适配HashSet的要求:
use std::collections::HashSet; #[derive(PartialEq, Hash, Eq)] enum Animal { Duck { name: String }, Dog { breed: String }, } trait HasVoice { fn talk(&self); } impl HasVoice for Animal { fn talk(&self) { match self { Animal::Duck { .. } => println!("duck quack!"), Animal::Dog { .. } => println!("dog bark!"), } } } fn test() { let duck = Animal::Duck { name: "duckie".to_string(), }; let dog = Animal::Dog { breed: "labrador".to_string(), }; let mut quack_set: HashSet<Animal> = HashSet::new(); quack_set.insert(duck); quack_set.insert(dog); // 遍历调用talk方法 for animal in quack_set { animal.talk(); } }
方案二:手动为trait对象实现Hash和相等判断
如果一定要用trait对象,需给Box<dyn HasVoice>手动实现Hash、PartialEq、Eq。核心逻辑是:不同类型的对象绝对不相等,相同类型则比较内部数据,需在trait中添加类型判断方法:
use std::collections::HashSet; use std::any::{Any, TypeId}; use std::hash::{Hash, Hasher}; trait HasVoice { fn talk(&self); // 添加方法用于类型判断和向下转型 fn as_any(&self) -> &dyn Any; } #[derive(PartialEq, Hash, Eq)] struct Duck { name: String, } #[derive(PartialEq, Hash, Eq)] struct Dog { breed: String, } impl HasVoice for Duck { fn talk(&self) { println!("duck quack!") } fn as_any(&self) -> &dyn Any { self } } impl HasVoice for Dog { fn talk(&self) { println!("dog bark!") } fn as_any(&self) -> &dyn Any { self } } // 手动为Box<dyn HasVoice>实现PartialEq impl PartialEq for Box<dyn HasVoice> { fn eq(&self, other: &Self) -> bool { // 先判断类型是否相同 self.as_any().type_id() == other.as_any().type_id() // 类型相同则向下转型比较 && self.as_any().downcast_ref::<Duck>().map_or(false, |a| { other.as_any().downcast_ref::<Duck>().map_or(false, |b| a == b) }) || self.as_any().downcast_ref::<Dog>().map_or(false, |a| { other.as_any().downcast_ref::<Dog>().map_or(false, |b| a == b) }) } } // 实现Eq impl Eq for Box<dyn HasVoice> {} // 手动为Box<dyn HasVoice>实现Hash impl Hash for Box<dyn HasVoice> { fn hash<H: Hasher>(&self, state: &mut H) { // 先哈希类型ID,再哈希内部数据 self.as_any().type_id().hash(state); if let Some(duck) = self.as_any().downcast_ref::<Duck>() { duck.hash(state); } else if let Some(dog) = self.as_any().downcast_ref::<Dog>() { dog.hash(state); } } } fn test() { let duck = Box::new(Duck { name: "duckie".to_string(), }) as Box<dyn HasVoice>; let dog = Box::new(Dog { breed: "labrador".to_string(), }) as Box<dyn HasVoice>; let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new(); quack_set.insert(duck); quack_set.insert(dog); for animal in quack_set { animal.talk(); } }
说明
方案一简洁高效,适合类型数量固定的场景;方案二更灵活,但需手动维护类型判断逻辑,新增类型时要同步修改PartialEq和Hash的实现,扩展性较差。
内容的提问来源于stack exchange,提问作者Wojciech Owczarczyk
相关产品推荐
相关产品推荐

