• Rust如何创建迭代器
  • 发布于 2个月前
  • 301 热度
    0 评论
  • 遥歌
  • 2 粉丝 35 篇博客
  •   
Rust 标准库文档详细解释了各类型所提供的迭代器的种类。标准库遵循一些一般惯例,以帮助你获得所需。

iter 和 iter_mut
大多数集合类型提供了 iter 和 iter_mut 方法,它们返回该集合类型的迭代器,迭代器产生对每个 item 的共享引用或可变引用。像数组切片 &[T]、&mut [T] 也有iter 和 iter_mut 方法。如果你不打算用 for 循环,那么这俩方法是获取迭代器的最常见方法:
let v = vec![4, 20, 12, 8, 6];
let mut iterator = v.iter(); 
assert_eq!(iterator.next(), Some(&4)); 
assert_eq!(iterator.next(), Some(&20)); 
assert_eq!(iterator.next(), Some(&12)); 
assert_eq!(iterator.next(), Some(&8)); 
assert_eq!(iterator.next(), Some(&6)); 
assert_eq!(iterator.next(), None);
该迭代器的 项(item) 的类型是 &i32:每次调用 next 都会产生对下一个元素的引用,直到到达 vector 的末端。每种类型都可以依照其目的自由地实现 iter 和 iter_mut。std::path::Path 的iter 方法返回一个迭代器,每次迭代产生一个 path Component:
use std::ffi::OsStr; 
use std::path::Path;

let path = Path::new("C:/Users/JimB/Downloads/Fedora.iso"); 
let mut iterator = path.iter(); 
assert_eq!(iterator.next(), Some(OsStr::new("C:")));
assert_eq!(iterator.next(), Some(OsStr::new("Users")));
assert_eq!(iterator.next(), Some(OsStr::new("JimB")));
...
该迭代器的 item 类型是 &std::ffi::OsStr,是对 操作系统调用 所接受的字符串切片的借用。

如果某个类型上有不止一种常见的迭代方式,那么该类型通常会为每一种迭代方式提供特定的方法,毕竟单纯地叫作 iter 将会显得模棱两可。例如,在字符串切片类型 &str 上就没有 iter方法。相应地,如果 s 是 &str,那么 s.bytes() 会返回一个迭代器,该迭代器产生 s 的每个字节,而 s.chars() 则会将内容解释为 UTF-8 并产生每个 Unicode 字符。

IntoIterator 的实现
当一个类型实现了 IntoIterator,你便可以调用它的 into_iter 方法,就像 for 循环那样:
// 通常你应该用 HashSet,但是它的迭代顺序是
// 不确定的,所以在例子中 BTreeSet 效果更好。
use std::collections::BTreeSet;
let mut favorites = BTreeSet::new();
favorites.insert("Lucy in the Sky With Diamonds".to_string());
favorites.insert("Liebesträume No. 3".to_string());

let mut it = favorites.into_iter();
assert_eq!(it.next(), Some("Liebesträume No. 3".to_string()));
assert_eq!(it.next(), Some("Lucy in the Sky With Diamonds".to_string()));
assert_eq!(it.next(), None);
大多数集合实际上提供了 IntoIterator 的几种实现,分别用于共享引用(&T)、可变引用(&mut T)和移动(T):
给定一个对集合的共享引用,into_iter 返回一个迭代器,该迭代器产生对其各项的共享引用。例如,在前面的代码中,调用 (&favorites).into_iter() 将返回一个迭代器,其项的类型是 &String。

给定一个对集合的可变引用,into_iter 返回一个迭代器,该迭代器产生对其各项的可变引用。例如,假设 vector 是 Vec<String>,调用 (&mut vector).into_iter() 将返回一个迭代器,其项的类型是 &mut String。

当集合按值传递时,into_iter 返回一个迭代器,该迭代器取得集合的所有权并按值返回各项;各项的所有权从集合转移到消费者,而原始集合在这个过程中被消耗。例如,在前面的代码中,调用 favorites.into_iter() 返回一个迭代器并按值产生各个字符串;消费者获得各个字符串的所有权。当迭代器被 drop 时,BTreeSet 中任何剩余的元素也会被 drop,并且集合本身也被丢弃。

由于 for 循环将 IntoIterator::into_iter 应用于其操作数,上面讲的三种实现对应了以下惯例,分别用于迭代对集合的共享引用、可变引用,或消耗集合并获得其元素的所有权:
// 堆代码 duidaima.com
for element in &collection { ... } 
for element in &mut collection { ... } 
for element in collection { ... }
每个 for 都会导致对前文列出的 IntoIterator 实现之一的调用。

当然,并非每个类型都提供所有三种实现。例如,HashSet、BTreeSet 和 BinaryHeap 没有在可变引用上实现 IntoIterator,因为修改其元素可能会违反该类型的不变性:被修改的值可能有不同的哈希值,或者造成元素乱序。其他类型确实支持 mutation,但也只是部分支持。例如,HashMap 和 BTreeMap 对其条目的值产生了可变引用,但对其键的引用却是共享的,原因与前面给出的类似。

总的原则是,迭代应该有效且可预测。所以 Rust 并没有提供昂贵的或可能表现诡异的实现,而是选择完全省略它们。

切片实现了三个 IntoIterator 变体中的两个;因为它们不拥有自己的元素,所以没有 "按值 "的情况。相应地,针对 &[T] 或 &mut[T] 的 into_iter 方法所返回的迭代器会产生对元素的共享或可变引用。如果你把底层切片类型 [T] 想象成某种集合,这就能很好地融入整个迭代器的运作模式。

你可能已经注意到,前两个 IntoIterator 变体——共享引用和可变引用——相当于在引用上调用 iter 或 iter_mut。为什么 Rust 同时提供两种方式呢?

IntoIterator 是使 for 循环工作的底层保障,显然是必要的。但是当你不使用 for 循环时,写 favorites.iter() 比写 (&favorites).into_iter() 会更清楚。你经常需要通过共享引用进行迭代,所以 iter 和 iter_mut 仍然有价值,毕竟它们更顺手。

IntoIterator 在泛型代码中也很有用:可以使用 T: IntoIterator 来限制类型变量 T,使其成为可以被迭代的类型。或者,可以用 T: IntoIterator<Item=U> 来进一步要求迭代产生特定的类型 U。例如下面的函数,它从任意可迭代的类型中转储其值,且其迭代器的项可以用 "{:?}" 格式打印:
use std::fmt::Debug;
fn dump<T, U>(t: T)
    where T: IntoIterator<Item=U>,
          U: Debug
{
    for u in t {
        println!("{:?}", u);
    }
}
你不能用 iter 和 iter_mut 来写这个泛型函数,因为它们不是任何 trait 的方法。只是大多数可迭代类型恰好包含这两个名字的方法而已。

from_fn 和 successor
产生序列值的另一个简单而通用的方法是提供一个返回它们的闭包。给定一个返回 Option<T> 的函数 F,std::iter::from_fn 可以返回一个迭代器,该迭代器直接调用 F 函数来产生值。比如:
use rand::random;
use std::iter::from_fn;
//  生成 1000 条随机线段,这些线段的端点均匀地分布在区间[0, 1]内。
let lengths: Vec<f64> =
    from_fn(|| Some((random::<f64>() - random::<f64>()).abs()))
    .take(1000)
    .collect();
例中调用 from_fn 来生成一个产生随机数的迭代器。由于迭代器总是返回 Some,所以序列永不结束,但我们通过调用 take(1000) 来限制它返回前 1000 个元素。然后用 collect 从迭代结果中建立 vector。这也是一种初始化 vector 的有效方法。若是各个项依赖于前一个项,std::iter::successors 函数可以很好地胜任此工作。你需要提供一个初始项和一个函数,该函数接收一个项并返回下一个项的 Option。如果它返回 None,迭代就结束了。例如:
use num::Complex;
use std::iter::successors;

fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> { 
    let zero = Complex { re: 0.0, im: 0.0 };
    successors(Some(zero), |&z| { Some(z * z + c) })
        .take(limit)
        .enumerate()
        .find(|(_i, z)| z.norm_sqr() > 4.0) .map(|(i, _z)| i)
}
从 zero 开始,通过重复对最后一个点进行平方,并加上参数 c,调用 successor 在复平面上产生一个点序列。
from_fn 和 successors 都接受 FnMut 闭包,所以它可以从周围的作用域中捕获并修改变量。例如,下面这个 fibonacci 函数使用 move 闭包来捕获变量并将其作为运行状态:
fn fibonacci() -> impl Iterator<Item=usize> { 
    let mut state = (0, 1);
    std::iter::from_fn(move || {
        state = (state.1, state.0 + state.1);
        Some(state.0) 
    })
} 

assert_eq!(fibonacci().take(8).collect::<Vec<_>>(),
        vec![1, 1, 2, 3, 5, 8, 13, 21]);
需要注意的是:from_fn 和 successor 方法足够灵活,致使你可以通过复杂的闭包,把对迭代器的任何调用变成对一个元素的单一调用。但这样做却忽视了迭代器的价值——即明确数据如何在计算中流转,并为常见模式使用标准命名。在使用这两个迭代器之前,请确保你已经熟悉了其他迭代器方法,因为通常有更好的方法来完成工作。

drain 方法
许多集合类型提供了 drain 方法,它接收一个对集合的可变引用,并返回一个迭代器,该迭代器将每个元素的所有权传递给消费者。然而,与 into_iter() 方法“按值”消费集合所不同的是,drain 只是借用了一个对集合的可变引用,当迭代器被 drop 时,它会从集合中删除所有剩余元素,使其为空。一些可以通过范围进行索引的类型(比如 String、vector 和 VecDeque),drain 方法会移除一定范围的元素,而不是整个序列:
use std::iter::FromIterator;

let mut outer = "Earth".to_string();
let inner = String::from_iter(outer.drain(1..4));

assert_eq!(outer, "Eh");
assert_eq!(inner, "art");
如果你确实需要移除整个序列,请使用全部范围 ... 作为参数。
用户评论