有什么方法可以在Rust中模拟泛型关联类型/关联类型构造函数?

时间:2019-01-12 16:17:16

标签: generics rust associated-types

我正在Rust中制作一个图形处理模块。该模块的核心对具有多个容器的想法进行了建模,这些容器将数据保存在图中。例如,我可能有一个图,其内部结构是HashMapAdjacencyMatrix等。

这些容器必须实现一个特征:

trait GraphData<V> {
    fn has_edge(&self, v: &V, u: &V) -> bool;
    fn nodes(&self) -> Iterator<V>; // Here's the problem...
}

我不能只在特征定义中返回一个特征。我知道我必须使用trait对象,但是我不想Box。我想使容器提供自己的NodeIter结构。但是,我会陷入Associated type constructors, part 1: basic concepts and introduction中解释的相同问题。这篇文章解释了现在在Rust中不存在的关联类型构造函数(ATC)。我的GraphData类似于所描述的通用Collection

是否可以使用任何变通办法来“模拟” ATC或可以在这种情况下使用的特定于Rust的任何模式?

我不想依靠动态调度,而想使用Boxdyn关键字。

我想为每种类型的图形容器定义一个结构NodeIter 我在模块中创建并在容器本身的实现内添加“节点”。但是,我发现代码重用性很差。

2 个答案:

答案 0 :(得分:5)

您所描述的问题已通过普通associated types解决。它不需要generic associated types,也就是关联的类型构造函数。这已经可以在稳定的Rust中使用。

trait GraphData<V> {
    type Nodes: Iterator<Item = V>;
    fn has_edge(&self, v: &V, u: &V) -> bool;
    fn nodes(&self) -> Self::Nodes;
}

struct Graph<V> {
    nodes: Vec<V>,
    edges: Vec<(V, V)>,
}

impl<V: Clone + Eq> GraphData<V> for Graph<V> {
    type Nodes = vec::IntoIter<V>;
    fn has_edge(&self, u: &V, v: &V) -> bool {
        self.edges.iter().any(|(u1, v1)| u == u1 && v == v1)
    }
    fn nodes(&self) -> Self::Nodes {
        self.nodes.clone().into_iter()
    }
}

Nodes没有类型或生存期参数(不是Nodes<T>Nodes<'a>),因此不是通用的。

如果您希望Nodes类型能够保留对Self的引用(以避免使用clone()),那么然后 Nodes需要与生命周期参数通用。不过,这并不是避免使用clone()的唯一方法:您可以使用Rc

答案 1 :(得分:2)

正如the answer by Anders Kaseorg所解释的:如果可以克隆包含顶点的Vec,那么您可能不需要这里的GAT。但是,这可能不是您想要的。相反,您通常希望有一个引用原始数据的迭代器。

要实现这一目标,实际上,您实际上希望使用GAT。但是,由于它们还不是该语言的一部分,因此让我们解决您的主要问题:是否有任何方法可以模拟泛型关联类型?我实际上写了一篇有关该主题的非常广泛的博客文章:{{3} }。

文章摘要:

  • 最简单的方法是将迭代器装箱并将其作为特征对象返回:

    fn nodes(&self) -> Box<dyn Iterator<&'_ V> + '_>
    

    如您所说,您不想要那样,所以就出来了。

  • 您可以向特征添加一个生命周期参数,并在关联的类型和&self接收者中使用该生命周期:

    trait GraphData<'s, V: 's> {
        type NodesIter: Iterator<Item = &'s V>;
        fn nodes(&'s self) -> Self::NodesIter;
    }
    
    struct MyGraph<V> {
        nodes: Vec<V>,
    }
    
    impl<'s, V: 's> GraphData<'s, V> for MyGraph<V> {
        type NodesIter = std::slice::Iter<'s, V>;
        fn nodes(&'s self) -> Self::NodesIter {
            self.nodes.iter()
        }
    }
    

    这有效!但是,现在您的特征中有一个烦人的生命周期参数。在您的情况下,这可能很好(除了烦人),但实际上在某些情况下可能是关键问题,因此这可能对您不起作用。

  • 您可以通过具有一个辅助特性,将生命周期参数推到更深的层次,该特性在生命周期到类型之间都充当类型级别函数。这使情况变得不那么令人讨厌,因为lifetime参数不再是您的主要特征,但是它受到与先前解决方法相同的限制。

  • 您还可以走完全不同的路径,并编写一个包含对图形的引用的迭代器包装器。

    这只是一个粗略的草图,但是基本思想有效:您的实际内部迭代器不包含对图形的任何引用(因此其类型不需要self的生命周期)。而是将图形引用存储在特定类型Wrap中,并在每次next调用时传递给内部迭代器。

    赞:

    trait InnerNodesIter { /* ... */ }
    
    struct Wrap<'graph, G: GraphData, I: InnerNodesIter> {
        graph: &'graph G,
        iter: I,
    }
    
    type NodesIterInner: InnerNodesIter;
    fn nodes(&self) -> Wrap<'_, Self, Self::NodesIterInner>;
    

    然后,您可以为Iterator实现Wrap。您只需要一些与内部迭代器的接口,即可将引用传递给图形。类似于fn next(&mut self, graph: &Graph) -> Option<...>。您需要在InnerNodesIter中定义接口。

    这当然很冗长。而且它可能还会变慢一些,具体取决于迭代器的工作方式。

简短而可悲的总结是:没有一种令人满意的解决方法可以在每种情况下使用。


在这种情况下,我的观点:我从事的项目确实多次发生这种情况。就我而言,我只是使用了Box解决方案,因为它非常简单并且可以正常工作。唯一的缺点是速度(分配和动态分配),但是分配不会在紧密的循环中发生(除非您有大量的图,每个图只有很少的节点-不太可能),并且优化器可能能够大多数情况下取消动态调用虚拟化的方式(毕竟,实型信息仅在一个函数边界之内)。