从容器中移除trait对象:地址转usize的方式是否正确?
用Trait对象地址转usize实现取消订阅的正确性及更优方案
原实现的正确性分析
这种方式能在多数场景下运行,但并不安全。它依赖对象内存地址作为唯一标识,存在两个核心问题:
- 内存地址可复用:当某个
Observer被销毁后,后续分配的新对象可能复用同一个内存地址,会导致unsubscribe误删无关对象。 - 裸指针转换不安全:
self as *const Self as usize属于裸指针操作,绕过了Rust的安全检查机制,不符合Rust的安全编程范式,存在潜在的未定义行为风险。
更安全、符合Rust风格的实现方案
推荐两种惯用实现方式:
方式一:利用Rc的指针比较
因为Subject中存储的是Rc<RefCell<dyn IObserver>>,可以直接通过Rc::ptr_eq判断是否为同一个对象,这是Rust标准库提供的安全指针比较方式:
use std::rc::Rc; use std::cell::RefCell; trait IObserver { fn update(&mut self, message: &str); } struct Observer; impl IObserver for Observer { fn update(&mut self, message: &str) { // 实现具体更新逻辑 } } struct Subject { observers: Vec<Rc<RefCell<dyn IObserver>>> } impl Subject { fn unsubscribe(&mut self, observer: &Rc<RefCell<dyn IObserver>>) { self.observers.retain(|x| !Rc::ptr_eq(x, observer)); } fn subscribe(&mut self, observer: Rc<RefCell<dyn IObserver>>) { self.observers.push(observer); } }
这种方式的优势:
- 完全安全:
Rc::ptr_eq是标准库提供的安全API,不会触发未定义行为。 - 规避地址复用:只要
Rc实例存在,对应对象就不会被销毁,内存地址不会被复用。
方式二:为Observer分配唯一ID
如果场景不适合直接传递Rc,可以给每个Observer分配全局唯一ID,用原子类型保证线程安全(单线程场景可简化为普通整数):
use std::sync::atomic::{AtomicUsize, Ordering}; use std::rc::Rc; use std::cell::RefCell; static NEXT_OBSERVER_ID: AtomicUsize = AtomicUsize::new(0); trait IObserver { fn update(&mut self, message: &str); fn id(&self) -> usize; } struct Observer { id: usize, } impl Observer { fn new() -> Self { let id = NEXT_OBSERVER_ID.fetch_add(1, Ordering::Relaxed); Self { id } } } impl IObserver for Observer { fn update(&mut self, message: &str) { // 实现具体更新逻辑 } fn id(&self) -> usize { self.id } } struct Subject { observers: Vec<Rc<RefCell<dyn IObserver>>> } impl Subject { fn unsubscribe(&mut self, observer: &dyn IObserver) { let target_id = observer.id(); self.observers.retain(|x| x.borrow().id() != target_id); } }
这种方式的优势:
- 完全脱离内存地址依赖,彻底避免地址复用问题。
- 灵活性高:即使Observer被移动或销毁(只要ID已记录),也能准确识别目标。
内容的提问来源于stack exchange,提问作者Kullackov
相关产品推荐
相关产品推荐

