Rust观察者模式实现遇类型不匹配错误,求解决方案
问题分析
编译错误的核心是泛型 trait 对象的类型不兼容:你存储的是Arc<dyn Subscriber<dyn Message>>,但注册的是Arc<dyn Subscriber<T>>(T是具体的Message类型)。Subscriber<T>和Subscriber<dyn Message>是不同的trait对象类型,Rust不允许直接转换,因为泛型trait的类型参数不具备协变性,且无法保证类型安全。
解决方案
引入一个类型擦除的中间trait,将具体类型的订阅者统一转换成这个无泛型的trait对象存储,同时在内部处理类型转换和安全检查。
步骤1:修改Message trait,添加实例方法
为了在类型转换失败时能输出有用信息,给Message添加一个实例方法id():
pub trait Message { fn id() -> u8 where Self: Sized; // 实例方法,返回当前消息类型的id fn id(&self) -> u8 { <Self as Message>::id() } }
步骤2:定义类型擦除的ErasedSubscriber trait
这个trait不依赖具体消息类型,只处理dyn Message:
pub trait ErasedSubscriber { fn receive_erased(&self, msg: &Box<dyn Message>); }
步骤3:为所有Subscriber<T>自动实现ErasedSubscriber
通过泛型实现,让任何实现了Subscriber<T>的类型自动具备ErasedSubscriber的能力,内部处理类型向下转换:
impl<T, S> ErasedSubscriber for S where T: Message, S: Subscriber<T>, { fn receive_erased(&self, msg: &Box<dyn Message>) { // 尝试将动态类型的消息向下转换为订阅者期望的具体类型 let concrete_msg = msg.downcast_ref::<T>().unwrap_or_else(|| { panic!( "订阅者收到不匹配的消息类型:期望ID {},实际ID {}", T::id(), msg.id() ) }); // 调用订阅者的实际处理方法 self.receive(concrete_msg); } }
步骤4:简化Subscriber trait的方法参数
原receive方法的参数是&Box<T>,冗余且低效,改成直接接收&T:
pub trait Subscriber<T> where T: Message, { fn receive(&self, msg: &T); }
步骤5:修改Publisher的存储结构
将订阅者存储为Arc<dyn ErasedSubscriber>,而不是泛型的Subscriber:
pub struct Publisher<'a, 'b> { subscribers: HashMap< u8, ( fn(&[u8]) -> Box<dyn Message + 'a>, Arc<dyn ErasedSubscriber + 'b>, ), >, }
步骤6:调整register_subscriber方法
将传入的具体订阅者转换为ErasedSubscriber trait对象:
pub fn register_subscriber<T>(&mut self, subscriber: Arc<dyn Subscriber<T> + 'b>) where T: Message + 'a, T: for<'c> From<&'c [u8]>; { self.subscribers.insert( T::id(), ( |data| Box::new(T::from(data)), subscriber as Arc<dyn ErasedSubscriber + 'b>, ), ); }
步骤7:修改publish方法
调用ErasedSubscriber的receive_erased方法:
pub fn publish(&self, id: u8, data: &[u8]) { if let Some((message_factory, subscriber)) = self.subscribers.get(&id) { let message = message_factory(data); subscriber.receive_erased(&message); } }
调整具体订阅者的实现
对应修改ConcreteSubscriber的receive方法参数:
impl Subscriber<ConcreteMessage> for ConcreteSubscriber { fn receive(&self, _msg: &ConcreteMessage) { // 处理具体消息逻辑 } }
完整可编译代码
use std::{collections::HashMap, sync::Arc}; pub trait Message { fn id() -> u8 where Self: Sized; fn id(&self) -> u8 { <Self as Message>::id() } } pub trait Subscriber<T> where T: Message, { fn receive(&self, msg: &T); } pub trait ErasedSubscriber { fn receive_erased(&self, msg: &Box<dyn Message>); } impl<T, S> ErasedSubscriber for S where T: Message, S: Subscriber<T>, { fn receive_erased(&self, msg: &Box<dyn Message>) { let concrete_msg = msg.downcast_ref::<T>().unwrap_or_else(|| { panic!( "订阅者收到不匹配的消息类型:期望ID {},实际ID {}", T::id(), msg.id() ) }); self.receive(concrete_msg); } } pub struct Publisher<'a, 'b> { subscribers: HashMap< u8, ( fn(&[u8]) -> Box<dyn Message + 'a>, Arc<dyn ErasedSubscriber + 'b>, ), >, } impl<'a, 'b> Publisher<'a, 'b> { pub fn new() -> Self { Self { subscribers: HashMap::new(), } } pub fn register_subscriber<T>(&mut self, subscriber: Arc<dyn Subscriber<T> + 'b>) where T: Message + 'a, T: for<'c> From<&'c [u8]>; { self.subscribers.insert( T::id(), ( |data| Box::new(T::from(data)), subscriber as Arc<dyn ErasedSubscriber + 'b>, ), ); } pub fn publish(&self, id: u8, data: &[u8]) { if let Some((message_factory, subscriber)) = self.subscribers.get(&id) { let message = message_factory(data); subscriber.receive_erased(&message); } } } pub struct ConcreteMessage { // 成员省略 } impl Message for ConcreteMessage { fn id() -> u8 { 12 } } impl From<&[u8]> for ConcreteMessage { fn from(_data: &[u8]) -> Self { Self {} } } pub struct ConcreteSubscriber { // 成员省略 } impl Subscriber<ConcreteMessage> for ConcreteSubscriber { fn receive(&self, _msg: &ConcreteMessage) { // 处理消息逻辑 } } fn main() { let mut publisher = Publisher::new(); let subscriber = Arc::new(ConcreteSubscriber {}); publisher.register_subscriber(subscriber); publisher.publish(12, &[0, 1, 2, 3]); }
关键说明
- 类型擦除:通过
ErasedSubscriber将不同泛型参数的Subscriber统一成同一个trait对象,解决了存储时的类型不兼容问题。 - 类型安全:在
receive_erased中通过downcast_ref做类型检查,确保订阅者只会收到自己绑定的消息类型(由于Publisher是按消息ID注册和发布的,理论上不会出现类型不匹配,这里的panic是兜底处理)。 - 效率优化:将
&Box<T>改成&T,避免了不必要的Box操作,提升性能。
内容的提问来源于stack exchange,提问作者Mihai
相关产品推荐
相关产品推荐

