「Generics 解密」2-3

1,378 阅读3分钟

「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战


step 1

Map类型有一个极其灵活的类型定义,一个没有边界的泛型。新方法也没有边界,但它只在std::iter中可用。

让我们把这个应用到我们的Connector上:

struct Connector<I, F> {
     points_iter: I,
     op: F,
 }

 impl<I, F> Connector<I, F> {
     fn new(points_iter: I, op: F) -> Self {
         Connector { points_iter, op }
     }
 }

step 2

一般来说,std::iter类型在其内部迭代器上调用next并很容易改变其行为。其中许多类型,如Take::next,都是用非常简单的代码完成的。

标准库还告诉我们,impl block 是我们应该施加 trait bounds 的地方,因为这是严格意义上的必要的地方。请注意,迭代器类型有一个相关的类型叫做Item。语法 <Item = Something> 将其与通用类型区分开来。

这取代了我们简单地从Vec中抓取项目的旧代码。这将适用于任何能够创建迭代器的类型,其中Item是Point:

impl<Id, I, F> Iterator for Connector<I, F>
 where
     I: Iterator<Item = Point<Id>>,
     F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
 {
     type Item = Line<Id>;

     fn next(&mut self) -> Option<Self::Item> {
         let point_a = self.points_iter.next()?;
         let point_b = self.points_iter.next()?;
         let line = (self.op)(point_a, point_b);

         Some(line)
     }
 }

step 3

迭代器有产生其他迭代器的方法。如果一个例子还不够,可以在 Iterator trait 中还可以找到几十个其他的例子。

首先,我们需要改变Connect trait,因为它的方法会返回一个Connector,这一点已经被改变了。我们重写了Connector以使其更加灵活,所以让我们对Connect做同样的事情。我们将继续采用将 trait bounds 从定义中移出并移入实现的方式:

trait Connect<I, F> {
    fn connect(self, op: F) -> Connector<I, F>;
}

我们不能给Iterator添加方法,它不是我们代码的一部分。但是我们可以使用一个非常有用的技巧:我们将为每个实现了Iterator的类型实现我们的Connect方法。

impl<I, Id, F> Connect<Id, F> for I
where
    I: Iterator,
{
    fn connect(self, op: F) -> Connector<I, F> {
        Connector::new(self, op)
    }
}

这被称为自动实现。标准库经常使用它们。一个众所周知的例子是,为一个类型实现Display会给它一个ToString的自动实现。

Fn trait bound 也被删除了,因为Connect和Connector不再需要它了。由于选择性的灵活性,我们优雅地使我们的代码更简单,更强大。让我们看看我们的新代码的运行情况:

fn main() {
    let points = {
        let mut map = std::collections::HashMap::new();

        map.insert("A", Point { id: 1001 });
        map.insert("B", Point { id: 1002 });
        map.insert("C", Point { id: 1003 });
        map.insert("D", Point { id: 1004 });
        map.insert("E", Point { id: 1005 });

        map
    };

    let mut connector = points.into_values().connect(|left, right| Line {
        points: (left, right),
    });

    println!("This should be a line: {:?}.", connector.next());
    println!("This should be a line: {:?}.", connector.next());
    println!("This should be nothing: {:?}.", connector.next());
}

我们的代码比我们为Vec手动实现Connect时做的要多得多,但我们使用的行数却完全相同。想象一下,为std::collection中的每个类型手动实现Connect所需要的代码量。由于泛型的复杂性,我们只用一个代码块就能得到同样的东西,并且让编译器来填补空白。