Rust特征进阶指南系列一
在 Rust 中,当你希望函数参数是一个“特征(Trait)”时,写成
&dyn Trait 或 impl Trait(以及对应的引用形式 &impl Trait)是由 Rust 的内存管理和对象安全性决定的。其中:impl Trait获取所有权.函数调用后,原变量在外部可能无法再使用。函数内部可以随意销毁或移动该数据。
&impl Trait 不可变借用。函数调用后,原变量在外部依然可用。函数内部只能读取,不能修改或移动数据。
判断变量在调用后是否还能使用的唯一标准是:你传进去的是“数据本身”还是“数据的地址”?
在 Rust 中,
&[u8] 或 &String 这种引用本身就是一个对象。- 当你把
&[u8]传给impl AsRef<[u8]>时,虽然发生了“所有权转移”,但转移的是这个引用(指针)本身,而不是它指向的底层数据。 - 由于引用类型(
&T)默认实现了CopyTrait,它在传递时就像i32一样只是被复印了一份,原始变量并不会失效。
简单来说:特征(Trait)本身是一种“抽象规格”,它没有固定的大小。
1. 核心原因:DST(动态大小类型)
特征在 Rust 中属于 DST (Dynamically Sized Types)。
- 不同的类型(比如
String和&str)都可以实现同一个特征AsRef<[u8]>。 String的大小是 24 字节,而&str的大小是 16 字节。- Rust 编译器在编译时必须知道每个函数参数的大小。既然“特征”对应后的具体类型大小不一,编译器就无法直接在栈上给它分配空间。
结论: 你不能直接传递一个特征,你必须传递一个指向该特征的指针(引用或智能指针),因为指针的大小是固定的(在 64 位系统上是 16 字节,包含指向数据的指针和指向虚函数表的指针)。
2. 两种常见的写法及其区别
当你看到
&impl Trait 时,这其实是 静态分发(Static Dispatch) 的简写。写法 A:
&impl Trait (静态分发)fn print_it(item: &impl Display) {
println!("{}", item);
}
- 原理: 这叫 单态化(Monomorphization)。编译器会根据你调用的实际类型,生成对应版本的函数。如果你用
i32调用,它就生成一个处理i32的函数;用String调用,就再生成一个。 - 优点: 性能极高,没有运行时开销。
- 缺点: 如果调用类型过多,会导致编译出的二进制文件变大。
写法 B:
&dyn Trait (动态分发)fn print_it(item: &dyn Display) {
println!("{}", item);
}
- 原理: 这使用了特征对象(Trait Object)。它在运行时通过虚函数表(vtable)来查找方法。
- 优点: 减小二进制体积,允许在同一个集合里存放不同类型的对象(如
Vec<Box<dyn Trait>>)。 - 缺点: 有轻微的运行时性能损耗。
3. 为什么不能只写 Trait?
如果你尝试写
fn func(arg: Trait),编译器会直接报错:Sizedis not implemented for(dyn Trait + 'static)
这意味着编译器在问你:“我不知道这个参数占用多少内存,我该如何在栈上为你压栈?”
- 参数传递:多用
&impl Trait或泛型约束fn func<T: Trait>(arg: &T),因为性能最好。 - 存储/集合:如果需要存储多个实现了同一特征的不同类型,才会使用
&dyn Trait或Box<dyn Trait>。
参考资料:
浙公网安备 33010602011771号