使用通用迭代器而不是特定的列表类型

26

我对Rust非常陌生,之前主要使用C#和Java等语言。

在C#中,我们有IEnumerable<T> 可以用于迭代几乎任何类型的数组或列表。 C#还具有yield关键字,可用于返回惰性列表。以下是一个示例...

// Lazily returns the even numbers out of an enumerable
IEnumerable<int> Evens(IEnumerable<int> input)
{
    foreach (var x in input)
    {
        if (x % 2 == 0)
        {
            yield return x;
        }
    }
}

当然,这只是一个愚蠢的例子。我知道我可以使用Rust的map函数来做到这一点,但我想知道如何创建自己的方法来接受和返回通用迭代器。

据我所了解,Rust有通用迭代器,可以类似地使用,但它们超出了我的理解范围。我在文档中看到了IterIntoIteratorIterator类型,还有可能有更多,但没有很好的方法来理解它们。

有人能够提供清晰的示例来创建像上面那样的东西吗?谢谢!

P.S. 惰性方面是可选的。我更关心对特定列表和数组类型的抽象。


据我理解,您也在询问生成器 - 具体围绕yield关键字。Rust并没有完全拥有这些,但是您应该能够使用Iterator完成所有相同的事情。尽管实现迭代器时可能会更加复杂,但可以将其打出来。 - Shepmaster
@Shepmaster 是的,生成器!那就是我要找的计算机科学术语。这是次要的,但我明白 Iterator 如何帮助解决这个问题。 - jocull
4个回答

47

首先,忘记关于IntoIterator和其他特征或类型的事情。在Rust中,核心迭代特征是Iterator。它削减后的定义如下:

trait Iterator {
    type Item;  // type of elements returned by the iterator
    fn next(&mut self) -> Option<Self::Item>;
}

正如您可能知道的那样,您可以将迭代器视为某些结构内部的光标。 next() 方法将此光标向前移动,返回它先前指向的元素。当然,如果集合已耗尽,则没有要返回的内容,因此next()返回Option<Self::Item>而不仅仅是Self::Item

Iterator是一个特征,因此可以由特定类型实现。请注意,Iterator 本身不是可用作返回值或函数参数的正确类型-必须使用实现此特征的具体类型。

上述语句可能听起来太过严格-那么如何使用任意迭代器类型呢?但由于泛型,这并不是问题。如果要使函数接受任意迭代器,请在相应的参数中使其成为通用类型,添加对应类型参数的Iterator绑定:

fn iterate_bytes<I>(iter: I) where I: Iterator<Item=u8> { ... }

从函数返回迭代器可能会比较困难,但请见下文。

例如,&[T] 上有一个名为 iter() 的方法,它返回一个迭代器,该迭代器产生对切片中的引用。这个迭代器是 这个 结构体的实例。您可以在该页面上查看如何为 Iter 实现 Iterator

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    fn next(&mut self) -> Option<&'a T> { ... }
    ...
}

这个结构体保存了对原始切片的引用以及其中的一些迭代状态。它的next()方法会更新该状态并返回下一个值(如果有)。

任何类型实现了Iterator的值都可以在for循环中使用(实际上,for循环使用的是IntoIterator,但详见下文):

let s: &[u8] = b"hello";
for b in s.iter() {
    println!("{}", b);   // prints numerical value of each byte
}
现在,Iterator特质实际上比上面的更加复杂。它还定义了许多转换方法,这些方法会消耗它们所调用的迭代器,并返回一个新的迭代器,该迭代器以某种方式从原始迭代器中转换或过滤值。例如,enumerate()方法返回一个迭代器,该迭代器与元素的位置编号一起产生原始迭代器的值:
let s: &[u8] = b"hello";
for (i, b) in s.iter().enumerate() {
    println!("{} at {}", b, i);   // prints "x at 0", "y at 1", etc.
}

enumerate() 的定义如下:

trait Iterator {
    type Item;
    ...
    fn enumerate(self) -> Enumerate<Self> {
        Enumerate {
            iter: self,
            count: 0
        }
    }
    ...
}

Enumerate 是一个包含迭代器和计数器的结构体,并且实现了 Iterator<Item=(usize, I::Item)> 接口:

struct Enumerate<I> {
    iter: I,
    count: usize
}

impl<I> Iterator for Enumerate<I> where I: Iterator {
    type Item = (usize, I::Item);

    #[inline]
    fn next(&mut self) -> Option<(usize, I::Item)> {
        self.iter.next().map(|a| {
            let ret = (self.count, a);
            self.count += 1;
            ret
        })
    }
}

这是大多数迭代器转换的实现方式:每个转换都是一个包装结构体,它包装原始迭代器并通过委托给原始迭代器来实现Iterator trait和某种程度的值变换。例如,上面的例子中的 s.iter().enumerate() 返回一个类型为 Enumerate<Iter<'static, u8>> 的值。

请注意,虽然 enumerate() 直接在 Iterator trait 中定义,但它也可以是一个独立的函数:

fn enumerate<I>(iter: I) -> Enumerate<I> where I: Iterator {
    Enumerate {
        iter: iter,
        count: 0
    }
}

这种方法的工作方式非常相似-它只是使用隐式的Self类型参数而不是显式命名的参数。


你可能会想知道IntoIterator trait是什么。嗯,它只是一个方便的转换trait,可以由任何可以转换为迭代器的类型来实现:

pub trait IntoIterator where Self::IntoIter::Item == Self::Item {
    type Item;
    type IntoIter: Iterator;

    fn into_iter(self) -> Self::IntoIter;
}

例如,&'a [T] 可以转换为 Iter<'a, T>,因此它具有以下实现:

impl<'a, T> IntoIterator for &'a [T] {
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Iter<'a, T> {
        self.iter()  // just delegate to the existing method
    }
}

这个特性已经被大多数容器类型和对这些类型的引用所实现。事实上,for 循环就是使用了它——任何实现了 IntoIterator 特性的类型的值都可以在 in 子句中使用:

let s: &[u8] = b"hello";
for b in s { ... }

从学习和阅读的角度来看,这非常好,因为它具有更少的噪音(以iter()-like方法的形式)。它甚至允许像这样的事情:

let v: Vec<u8> = ...;

for i in &v { /* i is &u8 here, v is borrowed immutably */ }
for i in &mut v { /* i is &mut u8 here, v is borrowed mutably */ }
for i in v { /* i is just u8 here, v is consumed */ }
这是因为&Vec<T>&mut Vec<T>Vec<T>针对IntoIterator实现是不同的。每个Iterator都实现了IntoIterator,它执行一个身份转换(into_iter()只是返回调用它的迭代器),所以你也可以在for循环中使用Iterator实例。因此,在通用函数中使用IntoIterator是有意义的,因为它会让API更加方便易用。例如,上面的enumerate()函数可以重写为:
fn enumerate<I>(source: I) -> Enumerate<I::IntoIter> where I: IntoIterator {
    Enumerate {
        iter: source.into_iter(),
        count: 0
    }
}

现在,您可以看到如何使用泛型轻松地实现具有静态类型的转换。Rust 没有像 C# / Python 的 yield 这样的功能(但它是最受欢迎的功能之一,因此它可能会出现在语言中!),因此您需要明确地包装源迭代器。例如,您可以编写类似于上面的 Enumerate 结构,该结构执行所需的任务。

然而,最惯用的方法是使用现有的组合器来为您完成工作。例如,您的代码可以编写如下:

let iter = ...;  // iter implements Iterator<Item=i32>
let r = iter.filter(|&x| x % 2 == 0);  // r implements Iterator<Item=i32>
for i in r {
    println!("{}", i);  // prints only even items from the iterator
}

然而,在编写自定义组合函数时,使用组合子可能会变得难看,因为许多现有的组合函数接受闭包(例如上面的filter()),但是在Rust中,闭包实现为匿名类型的值,因此根本没有办法编写返回迭代器的函数签名:

fn filter_even<I>(source: I) -> ??? where I: IntoIter<Item=i32> {
    source.into_iter().filter(|&x| x % 2 == 0)
}

有几种方法可以解决这个问题,其中之一是使用特质对象

fn filter_even<'a, I>(source: I) -> Box<Iterator<Item=i32>+'a>
    where I: IntoIterator<Item=i32>, I::IntoIter: 'a
{
    Box::new(source.into_iter().filter(|&x| x % 2 == 0))
}

在这里,我们使用特质对象隐藏了由filter()返回的实际迭代器类型。请注意,为了使函数完全通用,我不得不添加一个生命周期参数以及相应的边界到Box特征对象和I::IntoIter关联类型中。这是必要的,因为I::IntoIter可能包含其中任意的生命周期(就像上面的Iter<'a, T>类型一样),并且我们必须在特质对象类型中指定它们(否则生命周期信息将会丢失)。

Iterator特质创建的特质对象本身实现了Iterator,因此您可以像往常一样继续使用这些迭代器:

let source = vec![1_i32, 2, 3, 4];
for i in filter_even(source) {
    println!("{}", i);  // prints 2 and 4
}

@jocull,哦,抱歉,那当然应该是IntoIterator。我已经更新了示例,并修复了其中的生命周期问题。现在它可以工作了:http://is.gd/7AZVst - Vladimir Matveev
谢谢!我看到示例也改变了,包括生命周期(我遇到了这个问题)。你能解释一下这里的生命周期是做什么的吗?感觉它与将内存移动到Box中有关,但整个内存模型对我来说是一个非常新的概念。 - jocull
谢谢!还有很多要学习(生命周期和箱子还有点模糊)。例如,我不确定如何借用盒装的迭代器并多次迭代它。也许最好的做法是总是将新集合推入向量中?http://is.gd/WNZiyA - jocull
1
@jocull,封装迭代器与多次迭代无关。任何迭代器只能被遍历一次。请记住,迭代器是单向光标,一旦到达末尾,它们就变得无用。如果您想多次迭代某个东西,您必须将其存储在某种“稳定”形式中,例如集合。 - Vladimir Matveev
2
好的,一些迭代器是可以被克隆的,但你提供的例子并没有“迭代器被克隆”的情况。cloned()只是另一个迭代器转换方法,它在这里有描述:http://doc.rust-lang.org/std/iter/trait.Iterator.html#method.cloned。如果T是可克隆的,它对于从Iterator<Item=&T>获取Iterator<Item=T>非常有用。 - Vladimir Matveev
显示剩余3条评论

6

这里是完整版的Map这是创建它的函数。

一个最简单的实现看起来应该像这样:

fn map<I, E, B, F>(i: I, f: F) -> Map<I, F> where
    F: FnMut(E) -> B,
    I: Iterator<Item=E>
{
    Map {iter: i, f: f}
}

pub struct Map<I, F> {
    iter: I,
    f: F,
}

impl<B, I: Iterator, F> Iterator for Map<I, F> where F: FnMut(I::Item) -> B {
    type Item = B;

    fn next(&mut self) -> Option<B> {
        self.iter.next().map(|a| (self.f)(a))
    }
}

Playpen链接。请注意,迭代器内部使用的map是在Option上的方法;这不是递归定义!

虽然写起来不太方便,但它真的很快!


现在,要为任意“可枚举”类型编写此代码,需要将map更改为

fn map<I, E, B, F>(i: I, f: F) -> Map<I::IntoIter, F> where
    F: FnMut(E) -> B,
    I: IntoIterator<Item=E>
{
    Map {iter: i.into_iter(), f: f}
}

IntoIterator基本上相当于IEnumerable,只不过它使用into_iter代替了GetEnumerator


我觉得我无法理解这个问题。我不明白为什么IteratorIntoIter特质可以存在,但不能作为有效的输入或返回类型——我至少希望它们能够使用Box或Borrow(因为大小未知)。我真的很想看到一个例子,其中代码没有使用或修改自std lib。你能否展示一个在不先将其收集到Vec中的my_vec.map(...)操作的返回值的例子?这可能吗? - jocull
我尝试设置使用&Iterator<Item=i32>作为参数的东西,并且已经接近成功,但仍然存在借用错误。http://is.gd/00LPZ6 - jocull
1
@jocull:next()需要&mut self,所以迭代器需要可变;为什么不像Veedrac提供的示例那样按值获取它呢? - Matthieu M.

0

为应用于迭代器的结构体实现迭代器特质。您只需要实现next方法。其他方法有默认实现。

无法创建适用于任何容器的迭代器。此目的所需的类型系统机制尚不存在。


我主要是想以通用的方式迭代诸如Vec或LinkedList之类的东西,而不是迭代自定义结构体。 - jocull
不可能创建一个适用于任何容器的迭代器。只需为 IntoIterator 实现即可。 - Veedrac
@Veedrac,你能解释一下IntoIterator吗?这里有很多特质! - jocull

0

我也正在从C#背景学习Rust。这是我的实现方式:

fn evens<'a>(input: impl Iterator<Item = &'a i32>) -> impl Iterator<Item = &'a i32> {
    input.filter(|x| (*x % 2) == 0)
}

这里是测试

#[test]
fn test_evens() {
    let input = vec![1, 2, 3];

    let mut iter = evens(input.iter());
    assert_eq!(iter.next(), Some(&2));
    assert_eq!(iter.next(), None);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接