正式开始
trait object 回顾
- 当我们在运行时想让某个具体类型,只表现出某个 trait 的行为,可以通过将其赋值给一个 dyn T,无论是 &dyn T,还是 Box<dyn T>,还是 Arc<dyn T>,都可以,这里,T 是当前数据类型实现的某个 trait
- 此时,原有的类型被抹去,Rust 会创建一个 trait object,并为其分配满足该 trait 的 vtable。
- 使用 trait object 的好处是,当在某个上下文中需要满足某个 trait 的类型,且这样的类型可能有很多,当前上下文无法确定会得到哪一个类型时,我们可以用 trait object 来统一处理行为
- trait object 也是一种延迟绑定,它让决策可以延迟到运行时
- trait object 把决策延迟到运行时,带来的后果是执行效率的打折。在 Rust 里,函数或者方法的执行就是一次跳转指令,而 trait object 方法的执行还多一步,它涉及额外的内存访问,才能得到要跳转的位置再进行跳转,执行的效率要低一些
- 如果要把 trait object 作为返回值返回,或者要在线程间传递 trait object,都免不了使用 Box 或者 Arc,会带来额外的堆分配的开销
trait object 实现原理
- 在编译 dyn T 时,Rust 会为使用了 trait object 类型的 trait 实现,生成相应的 vtable,放在可执行文件中(一般在 TEXT 或 RODATA 段)
2. 当 trait object 调用 trait 的方法时,它会先从 vptr 中找到对应的 vtable,进而找到对应的方法来执行
在函数中使用 trait object
我们可以在函数的参数或者返回值中使用 trait object
在参数中使用 trait object
use std::{error::Error, process::Command};
// 使用 type 关键字创建了一个 BoxedError 类型,是 Box 的别名,它是 Error trait 的 trait object,除了要求类型实现了 Error trait 外,它还有额外的约束:类型必须满足 Send / Sync 这两个 trait
pub type BoxedError = Box<dyn Error + Send + Sync>;
pub trait Executor {
fn run(&self) -> Result<Option<i32>, BoxedError>;
}
pub struct Shell<'a, 'b> {
cmd: &'a str,
args: &'b [&'a str],
}
impl<'a, 'b> Shell<'a, 'b> {
pub fn new(cmd: &'a str, args: &'b [&'a str]) -> Self {
Self { cmd, args }
}
}
impl<'a, 'b> Executor for Shell<'a, 'b> {
fn run(&self) -> Result<Option<i32>, BoxedError> {
let output = Command::new(self.cmd).args(self.args).output()?;
Ok(output.status.code())
}
}
/// 使用泛型参数。静态分发
// impl Executor 使用的是泛型参数的简化版本。分配在栈上
pub fn execute_generics(cmd: &impl Executor) -> Result<Option<i32>, BoxedError> {
cmd.run()
}
/// 使用 trait object: &dyn T。动态分发
/// &dyn Executor 和 Box<dyn Executor> 是 trait object,分配在堆上
/// 分配在堆上的 trait object 也可以作为返回值返回,比如Result<Option<i32>, BoxedError> 里使用了 trait object。
pub fn execute_trait_object(cmd: &dyn Executor) -> Result<Option<i32>, BoxedError> {
cmd.run()
}
/// 使用 trait object: Box<dyn T>。动态分发
pub fn execute_boxed_trait_object(cmd: Box<dyn Executor>) -> Result<Option<i32>, BoxedError> {
cmd.run()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_shall_work() {
let cmd = Shell::new("ls", &[]);
let result = cmd.run().unwrap();
assert_eq!(result, Some(0));
}
#[test]
fn execute_shall_work() {
let cmd = Shell::new("ls", &[]);
let result = execute_generics(&cmd).unwrap();
assert_eq!(result, Some(0));
let result = execute_trait_object(&cmd).unwrap();
assert_eq!(result, Some(0));
let boxed = Box::new(cmd);
let result = execute_boxed_trait_object(boxed).unwrap();
assert_eq!(result, Some(0));
}
}
在函数返回值中使用
pub trait Storage: Send + Sync + 'static {
...
/// 遍历 HashTable,返回 kv pair 的 Iterator
fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}
在数据结构中使用 trait object
以 snow 的代码为例,看 HandshakeState 这个用于处理 Noise Protocol 握手协议的数据结构,用到了哪些 trait object
pub struct HandshakeState {
pub(crate) rng: Box<dyn Random>,
pub(crate) symmetricstate: SymmetricState,
pub(crate) cipherstates: CipherStates,
pub(crate) s: Toggle<Box<dyn Dh>>,
pub(crate) e: Toggle<Box<dyn Dh>>,
pub(crate) fixed_ephemeral: bool,
pub(crate) rs: Toggle<[u8; MAXDHLEN]>,
pub(crate) re: Toggle<[u8; MAXDHLEN]>,
pub(crate) initiator: bool,
pub(crate) params: NoiseParams,
pub(crate) psks: [Option<[u8; PSKLEN]>; 10],
#[cfg(feature = "hfs")]
pub(crate) kem: Option<Box<dyn Kem>>,
#[cfg(feature = "hfs")]
pub(crate) kem_re: Option<[u8; MAXKEMPUBLEN]>,
pub(crate) my_turn: bool,
pub(crate) message_patterns: MessagePatterns,
pub(crate) pattern_position: usize,
}
新鲜知识点
- 想更好地学习 trait 和 trait object 的使用,snow 是一个很好的学习资料。
- 你可以顺着 CryptoResolver 梳理它用到的这几个主要的加密算法相关的 trait,看看别人是怎么定义 trait、如何把各个 trait 关联起来,以及最终如何把 trait 和核心数据结构联系起来的(小提示:Builder 以及 HandshakeState)