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

164 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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|类型系统:如何在实战中使用泛型编程?