携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
今天的课程主题是实战中的 trait object 使用方法。在运行时,我们只关注类型表现出某个 trait 的行为,而不关心其具体类型时就可以使用 trait object,用法时 &dyn T 或 Box。Trait object 是一个固定大小的胖指针,同时指向具体类型值和 vtable。
从本质上讲,trait object 和 泛型一样,是 Rust 实现多态的一种方式。Trait object 也是一种延迟绑定,可以推迟决策到运行时。Trait object 使用的是动态分配,因此在获得较高灵活性的同时,也失去了部分执行效率。
01-trait object 作为函数参数
Trait object 作为函数参数时的用法如下(基于动态分发):
/// 使用 trait object: &dyn T
pub fn execute_trait_object(cmd: &dyn Executor) -> Result, BoxedError> {
cmd.run()
}
/// 使用 trait object: Box
pub fn execute_boxed_trait_object(cmd: Box) -> Result, BoxedError> {
cmd.run()
}
02-trait object 作为函数返回值
Trait object 也可作为函数的返回值:
pub trait Storage: Send + Sync + 'static {
...
/// 遍历 HashTable,返回 kv pair 的 Iterator
fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}
上例中返回值的类型是 Result<T, E>,其中 T 为在堆上分配的 trait object,E 为 KvError。
03-trait object 作为数据结构的域
Trait object 除了在函数中使用,还可作为数据结构的域:
pub struct HandshakeState {
pub(crate) rng: Box<dyn Random>,
...
}
其实上述结构也可通过泛型实现,例如:
pub struct HandshakeState<T> {
pub(crate) rng: T,
...
}
但是,相比于 trait object,使用泛型有两个主要的缺点:
- 如果泛型个数比较多,代码就会比较臃肿,可读性降低;
- 任何使用数据结构的地方,都必须带着泛型以及泛型参数的约束,写起来不方便。
综上,使用 trait object 虽然会损失部分执行性能(由动态分配导致),但会提高代码可读性,而且代码只有一份实现。
在数据结构中使用 trait object 还有一个典型的场景:闭包。例如:
#[derive(Clone)]
pub struct AttributeGetter(
Arc<dyn Fn(&Instance, &mut Host) -> crate::Result<PolarValue> + Send + Sync>,
);
本节课程链接《24|类型系统:如何在实战中使用trait object?》
历史文章推荐