携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
到目前为止,我们已经学习了 Rust 中大部分基础支持,而且在实践课中我们也跟着老师一起构建了一个 kv server 系统。从某种程度上讲,我们也算是一个入门的 Rustaceans 了。从今天开始,我们开始跟着老师学习进阶知识了。今天的主题是如何在实战中使用泛型编程。
01-泛型数据结构的逐步约束
在数据结构的定义中使用泛型我们已经见过很多,例如 Box,Option 等。在定义时,泛型对待处理的类型做了一个抽象,也就是说,具体什么类型并不关心。到了实现阶段,根据具体的需求不同,可以对泛型中的抽象类型进行具体的限制。例如:
// 为所有的 Option<T> 实现 xxx
impl<T> Option<T> { .. }
// 为实现了 fmt::Debug 类型的 Option<T> 实现 Debug trait
impl<T> fmt::Debug for Option<T> where T: fmt::Debug {...}
上述就展示了对泛型数据结构的逐步约束,从没有任何约束到约束为实现了 Debug trait 的类型。
通过使用泛型参数,数据结构的定义者将决策交给了数据结构的使用者。这符合《代码整洁之道》中的观点:“架构师的工作不是作出决策,而是尽可能久地推迟决策,在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策”。决策的推迟也使得系统架构更加的灵活,面对未来的变更也就更从容。
02-泛型参数的常用场景
02.1-使用泛型参数延迟类型绑定
泛型参数最为普遍的用法便是用来延迟数据类型的绑定。回想之前实践课程中定义的 Service 数据结构:
pub struct Service<Store = MemTable> {
inner: Arc<ServiceInner<Store>>,
}
定义时不关心泛型的具体类型,仅到实现时才知道。这种用法太普遍了,也比较直观,容易理解。
02.2-泛型参数与幽灵数据
Rust 中的幽灵数据(PhantomData)有点类似于 Java 中的幽灵类型(Phantom Type)。第一次接触幽灵数据是在学习 Box时,其内部实现的 Unique 的定义中就用到了:
pub struct Unique<T: ?Sized> {
pointer: NonNull<T>,
// NOTE: this marker has no consequences for variance, but is necessary
// for dropck to understand that we logically own a `T`.
//
// For details, see:
// https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md#phantom-data
_marker: PhantomData<T>,
}
幽灵数据本身并不占用内存,即其大小为0,主要是用来通知编译器在数据结构的定义中未使用到,但在实现过程中需要使用的参数。如果没有幽灵数据,编译器在编译时会自动删除未使用的泛型参数。
02.3-泛型参数与 trait 的多个实现
泛型参数的另外一个常用用途就是实现泛型数据结构对同一个 trait 的不同实现。例如:
pub struct Linear;
pub struct Quadratic;
pub struct Equation<T> {...};
impl Iterator for Equation<Linear> {...}
impl Iterator for Equation<Quadratic> {...}
我们知道,泛型在编译期间会单态化,Equation 和 Equation 可以看作是不同的数据类型。
这样做的好处是:用泛型数据结构来统一相同的逻辑,用泛型参数的具体类型来处理变化的逻辑。
03-泛型函数的高级技巧
03.1-函数返回值为泛型
如果想要在函数中返回一个泛型,应当用 Box,即 trait object,例如(代码来自课程讲义):
pub trait Storage {
fn get_iter(&self, table: &str)
-> Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}
为什么不使用 impl Iterator 呢?原因是目前 Rust 尚不支持这种用法。使用 trait object 消除了类型的差异,把实现 Iterator 的不同类型统一到了相同的 trait object 下。
使用 trait object 的代价是:需要额外的一次堆分配 Box,而且动态分配会牺牲一点性能。
03.2-复杂的泛型参数如何声明?
定义复杂的数据类型时,特别是数据类型的泛型参数特别复杂时,例如泛型参数为闭包,如何做呢?例如(代码来自本节课件):
pub fn comsume_iterator<F, Iter, T>(mut f: F)
where
F: FnMut(i32) -> Iter, // F 是一个闭包,接受 i32,返回 Iter 类型
Iter: Iterator<Item = T>, // Item 为 T类型的 iterator
T: std::fmt::Debug, // 实现了 Debug trait 的类型 T
{
// 根据 F 的类型,f(10) 返回 iterator,所以可以用 for 循环
for item in f(10) {
println!("{:?}", item); // item 实现了 Debug trait,所以可以用 {:?} 打印
}
}
本节课程链接《23|类型系统:如何在实战中使用泛型编程?》