我正在Rust中制作一个图形处理模块。该模块的核心对具有多个容器的想法进行了建模,这些容器将数据保存在图中。例如,我可能有一个图,其内部结构是HashMap
或AdjacencyMatrix
等。
这些容器必须实现一个特征:
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的任何模式?
我不想依靠动态调度,而想使用Box
或dyn
关键字。
我想为每种类型的图形容器定义一个结构NodeIter
我在模块中创建并在容器本身的实现内添加“节点”。但是,我发现代码重用性很差。
答案 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
解决方案,因为它非常简单并且可以正常工作。唯一的缺点是速度(分配和动态分配),但是分配不会在紧密的循环中发生(除非您有大量的图,每个图只有很少的节点-不太可能),并且优化器可能能够大多数情况下取消动态调用虚拟化的方式(毕竟,实型信息仅在一个函数边界之内)。