blanket impl 的一个坑
现象
- 某个用 trait object 做类型擦除的模块,一次无关重构之后,接收端的
downcast恒失败,消息被静默丢掉。 - 重构前是好用的,重构后挂了。查到最后,问题只出在一行
use上。
我们的写法
Any 擦除的常见套路,trait 这么定义:
pub trait Msg: Debug + Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any + Send>;
}
impl<T> Msg for T
where
T: Debug + Send + Sync + Any,
{
fn as_any(&self) -> &dyn Any { self }
fn into_any(self: Box<Self>) -> Box<dyn Any + Send> { self }
}
接收端拿到 Box<dyn Msg>,再还原成具体类型:
let msg: Box<dyn Msg> = /* 反序列化得到 */;
match msg.as_any().downcast_ref::<RealMsg>() {
Some(m) => { /* 正常处理 */ }
None => { log::error!("downcast failed"); continue; }
}
为什么拿到的不是数据,是盒子
impl<T> Msg for T是 blanket impl,任何满足Debug + Send + Sync + Any的类型都会自动实现Msg。- 问题在于,
Box<dyn Msg>本身,也满足这一串约束。 - 也就是说,「盒子」自己也实现了一份
Msg:
// T = Box<dyn Msg> 时,等价于:
impl Msg for Box<dyn Msg> {
fn as_any(&self) -> &dyn Any { self } // self = &Box<dyn Msg>,是盒子!
}
- Rust 找方法是从外往里:先看
Box<dyn Msg>这一层,再看dyn Msg里面这一层。 - trait 方法只有在 trait 被
use进作用域时才会参与解析;而dyn Msg这个 trait object 的 vtable 方法,永远可用。 - 于是,有没有
use Msg,结果完全两样:
msg.as_any() 命中谁 |
downcast | |
|---|---|---|
没 use Msg |
blanket 不可见,走到 dyn Msg 的 vtable,拿到真实 RealMsg |
✅ 成功 |
use Msg 了 |
blanket 可见,外层 Box<dyn Msg> 自己的 as_any 命中,拿到盒子 |
❌ 失败 |
- 这就是「只加了一行
use Msg;,功能就挂掉」的原因。它没动逻辑,只是让 blanket 进了方法解析视野,把原本走 vtable 的调用截胡走了。
复现
mod lib {
use std::any::Any;
use std::fmt::Debug;
pub trait Msg: Debug + Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
}
impl<T> Msg for T where T: Debug + Send + Sync + Any {
fn as_any(&self) -> &dyn Any { self }
}
#[derive(Debug)]
pub struct RealMsg { pub x: i32 }
}
fn no_use_case() {
let b: Box<dyn lib::Msg> = Box::new(lib::RealMsg { x: 42 });
println!("{}", b.as_any().downcast_ref::<lib::RealMsg>().is_some()); // true
}
mod with_use {
use super::lib::Msg; // 只多这一行
pub fn test() {
let b: Box<dyn super::lib::Msg> = Box::new(super::lib::RealMsg { x: 42 });
println!("{}", b.as_any().downcast_ref::<super::lib::RealMsg>().is_some()); // false
}
}
rustc 1.81.0 实测:no_use 打出 true,with_use 打出 false。
怎么改
先剥到 dyn Trait 再调,就确定走 vtable 了,跟有没有 use 没关系:
// as_ref(),可读性好,推荐
msg.as_ref().as_any().downcast_ref::<RealMsg>()
// 或者显式解引用
(&*msg).as_any().downcast_ref::<RealMsg>()
// 或者消费式,会 move,要处理 unwrap
msg.into_any().downcast::<RealMsg>()
- 前面两种是一回事,都是先拿到
&dyn Msg。 dyn Msg是 unsized,不满足 blanket 的Sized约束,所以 blanket 在它这一层根本不参与,只能走 vtable。行为就确定下来了。
排查口诀
boxed.as_any()失败、(&*boxed).as_any()成功,基本就是 blanket 遮蔽。- 以后在
Box<dyn Trait>上直接调&self的 trait 方法,一律先as_ref()/&*剥壳,或者用消费式的into_any(self: Box<Self>)。
— 完 —
评论