23|类型系统:如何在实战中使用泛型编程?

272 阅读5分钟

正式开始

在《架构整洁之道》里 Uncle Bob 说:架构师的工作不是作出决策,而是尽可能久地推迟决策,在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策

  1. BufReader讲解在定义数据结构和实现数据结构时,如果使用了泛型参数,到底有什么样的好处
  2. 在实现时根据不同的需求为R指定不同的限制
pub struct BufReader<R> {
    inner: R,
    buf: Box<[u8]>,
    pos: usize,
    cap: usize,
}

/*
1. BufReader 对要读取的 R 做了一个泛型的抽象。也就是说,R 此刻是个 File,还是一个 Cursor,或者直接是 Vec,都不重要
*/
  1. impl BufReader 的代码根据不同的约束,分成了不同的代码块。类似于Swift中使用extension来将同一个类的不同功能拆分到不同文件一样

泛型参数的三种使用场景

  1. 使用泛型参数延迟数据结构的绑定
  2. 使用泛型参数和 PhantomData,声明数据结构中不直接使用,但在实现过程中需要用到的类型
  3. 使用泛型参数让同一个数据结构对同一个 trait 可以拥有不同的实现

用泛型参数做延迟绑定

/// KV server中的Service 数据结构
pub struct Service<Store = MemTable> {
    inner: Arc<ServiceInner<Store>>,
}
/*
1. 它使用了一个泛型参数 Store,并且这个泛型参数有一个缺省值 MemTable
2. 指定了泛型参数缺省值的好处是,在使用时,可以不必提供泛型参数,直接使用缺省值
3. 这个泛型参数在随后的实现中可以被逐渐约束
*/

impl<Store> Service<Store> {
    pub fn new(store: Store) -> Self { ... }
}

impl<Store: Storage> Service<Store> {
    pub fn execute(&self, cmd: CommandRequest) -> CommandResponse { ... }
}

使用泛型参数和幽灵数据(PhantomData)提供额外类型

  1. Rust 知道 Phantom Type 的必要性,所以开了一扇叫 PhantomData 的窗户:让我们可以用 PhantomData 来持有 Phantom Type
  2. PhantomData 被广泛用在处理,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数
  3. 在定义数据结构时,对于额外的、暂时不需要的泛型参数,用 PhantomData 来“拥有”它们,这样可以规避编译器的报错
  4. 它实际上长度为零,是个 ZST(Zero-Sized Type),就像不存在一样,唯一作用就是类型的标记

use std::marker::PhantomData;

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Identifier<T> {
    inner: u64,
    _tag: PhantomData<T>,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct User {
    id: Identifier<Self>,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Product {
    id: Identifier<Self>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn id_should_not_be_the_same() {
        let user = User::default();
        let product = Product::default();

        // 两个 id 不能比较,因为他们属于不同的类型
        // assert_ne!(user.id, product.id);

        assert_eq!(user.id.inner, product.id.inner);
    }
}

使用泛型参数来提供多个实现


use std::marker::PhantomData;

#[derive(Debug, Default)]
pub struct Equation<IterMethod> {
    current: u32,
    _method: PhantomData<IterMethod>,
}

// 线性增长
#[derive(Debug, Default)]
pub struct Linear;

// 二次增长
#[derive(Debug, Default)]
pub struct Quadratic;

impl Iterator for Equation<Linear> {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.current += 1;
        if self.current >= u32::MAX {
            return None;
        }

        Some(self.current)
    }
}

impl Iterator for Equation<Quadratic> {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.current += 1;
        if self.current >= u16::MAX as u32 {
            return None;
        }

        Some(self.current * self.current)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_linear() {
        let mut equation = Equation::<Linear>::default();
        assert_eq!(Some(1), equation.next());
        assert_eq!(Some(2), equation.next());
        assert_eq!(Some(3), equation.next());
    }

    #[test]
    fn test_quadratic() {
        let mut equation = Equation::<Quadratic>::default();
        assert_eq!(Some(1), equation.next());
        assert_eq!(Some(4), equation.next());
        assert_eq!(Some(9), equation.next());
    }
}

用泛型数据结构来统一相同的逻辑,用泛型参数的具体类型来处理变化的逻辑


/// A marker that indicates that the wrapping type is compatible with `AsyncProstReader` with Prost support.
#[derive(Debug)]
pub struct AsyncDestination;

/// a marker that indicates that the wrapper type is compatible with `AsyncProstReader` with Framed support.
#[derive(Debug)]
pub struct AsyncFrameDestination;

/// A wrapper around an async reader that produces an asynchronous stream of prost-decoded values
#[derive(Debug)]
pub struct AsyncProstReader<R, T, D> {
    reader: R,
    pub(crate) buffer: BytesMut,
    into: PhantomData<T>,
    dest: PhantomData<D>,
}
/*
1. 这个数据结构虽然使用了三个泛型参数,其实数据结构中真正用到的只有一个 R,它可以是一个实现了 AsyncRead 的数据结构
2. 另外两个泛型参数 T 和 D,在数据结构定义的时候其实并不需要,只是在数据结构的实现过程中,才需要用到它们的约束
*/
  1. T 是从 R 中读取出的数据反序列化出来的类型,在实现时用 prost::Message 约束
  2. D 是一个类型占位符,它会根据需要被具体化为 AsyncDestination 或者 AsyncFrameDestination

泛型函数的高级技巧

返回值携带泛型参数怎么办?

我们可以返回 trait object,它消除了类型的差异,把所有不同的实现 Iterator 的类型都统一到一个相同的 trait object 下

pub trait Storage {
    ...
    /// 遍历 HashTable,返回 kv pair 的 Iterator
    fn get_iter(&self, table: &str) -> 
        Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}
/*
1. 
对于 get_iter() 方法,并不关心返回值是一个什么样的 Iterator,只要它能够允许我们不断调用 next() 方法,获得一个 Kvpair 的结构,就可以了
2. 在实现里,使用了 trait object
3. 这里有一次额外的堆分配,其次动态分派会带来一定的性能损失
*/

复杂的泛型参数该如何处理?


pub fn comsume_iterator<F, Iter,  T>(mut f: F)
where
    F: FnMut(i32) -> Iter, // F 是一个闭包,接受 i32,返回 Iter 类型
    Iter: Iterator<Item = T>, // Iter 是一个 Iterator,Item 是 T 类型
    T: std::fmt::Debug, // T 实现了 Debug trait
{
    // 根据 F 的类型,f(10) 返回 iterator,所以可以用 for 循环
    for item in f(10) {
        println!("{:?}", item); // item 实现了 Debug trait,所以可以用 {:?} 打印
    }
}

小结

  1. 做系统设计,我们考虑 ROI(Return On Investment)时,要把 TCO(Total Cost of Ownership)也考虑进去

好用链接

The Rust programming Language

bufreader.rs源码

PhantomData

AsyncProstReader

async_prost

async-prost 库,可以把 TCP 或者其他协议中的 stream 里传输的数据,分成一个个 frame 处理。其中的 AsyncProstReader 为 AsyncDestination 和 AsyncFrameDestination 提供了不同的实现

AsyncProstReader中Stream的实现

futures