每日一 R「18」类型系统进阶(二)

159 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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,使用泛型有两个主要的缺点:

  1. 如果泛型个数比较多,代码就会比较臃肿,可读性降低;
  2. 任何使用数据结构的地方,都必须带着泛型以及泛型参数的约束,写起来不方便。

综上,使用 trait object 虽然会损失部分执行性能(由动态分配导致),但会提高代码可读性,而且代码只有一份实现。

在数据结构中使用 trait object 还有一个典型的场景:闭包。例如:

#[derive(Clone)]
pub struct AttributeGetter(
    Arc<dyn Fn(&Instance, &mut Host) -> crate::Result<PolarValue> + Send + Sync>,
);

本节课程链接《24|类型系统:如何在实战中使用trait object?


历史文章推荐