Android程序员初学Rust-迭代器

125 阅读6分钟

1.jpg

如果想遍历数组的内容,你需要定义:

  1. 一些状态来跟踪你在迭代过程中的位置,例如一个索引。
  2. 一个条件来确定迭代何时结束。
  3. 每次循环更新迭代状态的逻辑。
  4. 使用该迭代状态获取每个元素的逻辑。

C 风格的 for 循环中,你可以直接声明这些内容:

for (int i = 0; i < array_len; i += 1) {
    int elem = array[i];
}

Rust 中,我们将这种状态和逻辑捆绑到一个称为 “迭代器” 的对象中。

Rust 没有 C 风格的 for 循环,但我们可以用 while 来实现相同的功能:

let array = [2, 4, 6, 8];
let mut i = 0;
while i < array.len() {
    let elem = array[i];
    i += 1;
}

没有那么方便,是吧。

Iterator Trait

2.jpg

Iterator 特征定义了如何使用一个对象来生成一系列值。例如,如果我们想创建一个能生成切片元素的迭代器,代码大概如下:

struct SliceIter<'s> {
    slice: &'s [i32],
    i: usize,
}

impl<'s> Iterator for SliceIter<'s> {
    type Item = &'s i32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i == self.slice.len() {
            None
        } else {
            let next = &self.slice[self.i];
            self.i += 1;
            Some(next)
        }
    }
}

fn main() {
    let slice = &[2, 4, 6, 8];
    let iter = SliceIter { slice, i: 0 };
    for elem in iter {
        dbg!(elem);
    }
}

上面展示了 Rust 风格的 for 循环,和 C 风格的 for 循环不同的是:

  • 只有实现了 Iterator trait 才能使用 Rust 风格的 for 循环。
  • 没有下标。
  • 只有 for-in 这样的格式。

该迭代器是惰性的:创建迭代器仅仅是初始化结构体,不会做其他任何工作。在调用 next 方法之前,不会有任何实际操作发生。

迭代器并不一定需要是有限的!创建一个能永远生成值的迭代器完全是可行的。例如,像 0.. 这样的半开区间迭代器会一直运行,直到发生整数溢出。

SliceIter 示例是一个很好的包含引用的结构体示例,因此使用了生命周期标注。

我们可以稍微扩展一下 SliceIter,使之支持泛型:

struct SliceIter<'s, T> {
    slice: &'s [T],
    i: usize,
}

impl<'s, T> Iterator for SliceIter<'s, T> {
    type Item = &'s T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i == self.slice.len() {
            None
        } else {
            let next = &self.slice[self.i];
            self.i += 1;
            Some(next)
        }
    }
}

fn main() {

    let slice = &[2, 4, 6, 8];
    let iter = SliceIter { slice, i: 0 };
    for elem in iter {
        print!("{} ", elem);
    }
    
    println!();
    
    let slice = &["ab", "cd", "ef", "app"];
    let iter = SliceIter { slice, i: 0 };
    for elem in iter {
        print!("{} ", elem);
    }
}

// Output
// 2 4 6 8 
// ab cd ef app

迭代器辅助方法

3.jpg

除了定义迭代器行为的 next 方法外,Iterator 特征还提供 70 多个辅助方法,可用于构建定制化的迭代器。

fn main() {
    let result: i32 = (1..=10) // 创建一个从 1 到 10 的范围
        .filter(|x| x % 2 == 0) // 只保留偶数
        .map(|x| x * x) // 计算平方
        .sum(); // 求和

    println!("The sum of squares of even numbers from 1 to 10 is: {}", result);
}

// Output
// The sum of squares of even numbers from 1 to 10 is: 220

Iterator 特征为集合实现了许多常见的函数式编程操作(例如 mapfilterreduce 等)。你可以在这个特征中找到有关这些操作的所有文档。

这些辅助方法中的许多会接收原始迭代器,并生成具有不同行为的新迭代器。这些方法被称为 “迭代器适配器方法”。

有些方法如 sumcount,会消费迭代器并从中提取所有元素。

这些方法旨在链式调用,这样就能轻松构建出完全符合你需求的自定义迭代器。

Rust 的迭代器极为高效且具有很高的可优化性。即便通过组合多个适配器方法创建出复杂的迭代器,其生成的代码效率也能与同等的命令式实现不相上下。该特性和 Kotlin/Java 中的链式调用有很大的差异。如果不使用 sequence 的话,Kotlin/Java 中的链式调用会生成很多的中间产物,浪费内存。

collect 方法可让你从迭代器构建一个集合。

fn main() {
    let primes = vec![2, 3, 5, 7];
    let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>();
    println!("prime_squares: {prime_squares:?}");
}

// Output
// prime_squares: [4, 9, 25, 49]

任何迭代器都可以收集到 VecVecDeque 或 HashSet 中。生成键值对(即二元元组)的迭代器也可以收集到 HashMap 和 BTreeMap 中。

let result = [("apple", 1.0), ("google", 2.0), ("microsoft", 3.0), ("openai", 4.0)];
let company_map: HashMap<&str, f32> = result.iter().map(|(k, v)| (*k, *v)).collect();
println!("{:?}", company_map);

// Output
// {"openai": 4.0, "apple": 1.0, "google": 2.0, "microsoft": 3.0}

IntoIterator

4.jpg

Iterator 特征说明了创建迭代器后如何进行迭代。相关的 IntoIterator 特征定义了如何为某一类型创建迭代器。for 循环会自动使用该特征。

struct Grid {
    x_coords: Vec<u32>,
    y_coords: Vec<u32>,
}

impl IntoIterator for Grid {
    type Item = (u32, u32);
    type IntoIter = GridIter;
    fn into_iter(self) -> GridIter {
        GridIter { grid: self, i: 0, j: 0 }
    }
}

struct GridIter {
    grid: Grid,
    i: usize,
    j: usize,
}

impl Iterator for GridIter {
    type Item = (u32, u32);

    fn next(&mut self) -> Option<(u32, u32)> {
        if self.i >= self.grid.x_coords.len() {
            self.i = 0;
            self.j += 1;
            if self.j >= self.grid.y_coords.len() {
                return None;
            }
        }
        let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));
        self.i += 1;
        res
    }
}

fn main() {
    let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] };
    for (x, y) in grid {
        println!("point = {x}, {y}");
    }
}

如果想让 Grid 能够直接在 for 循环中使用,那么他就必须要实现 Iterator(提供迭代能力) 以及 IntoIterator(提供迭代器)。

IntoIterator 是让 for 循环得以工作的特征。集合类型(如 Vec<T>)以及对它们的引用(如 &Vec<T>&[T])都实现了这个特征。区间(Ranges)也实现了该特征。这就是为什么你可以使用 for i in some_vec {.. } 来遍历向量(vector),但 some_vec.next() 并不存在。

点击查看 IntoIterator 的文档。IntoIterator 的每一个实现都必须声明两种类型:

  • Item:要遍历的类型,比如 i8
  • IntoIter:由 into_iter 方法返回的 Iterator 类型。

请注意,IntoIterItem 是相关联的:迭代器必须具有相同的 Item 类型,这意味着它返回 Option<Item>

示例代码遍历了 xy 坐标的所有组合。

尝试在 main 函数中对 grid 进行两次遍历会失败,为什么呢?

IntoIterator::into_iter 会获取 self 的所有权。

通过为 &Grid 实现 IntoIterator 并创建一个通过引用进行遍历的 GridRefIter 来解决这个问题。如下代码示例中可以看到同时包含 GridIterGridRefIter 的版本。

struct Grid {
    x_coords: Vec<u32>,
    y_coords: Vec<u32>,
}

impl IntoIterator for Grid { // 支持 in Grid
    type Item = (u32, u32);
    type IntoIter = GridIter;
    fn into_iter(self) -> GridIter {
        GridIter { grid: self, i: 0, j: 0 }
    }
}

impl<'a> IntoIterator for &'a Grid { // 支持 in &Grid
    type Item = (u32, u32);
    type IntoIter = GridRefIter<'a>;
    fn into_iter(self) -> GridRefIter<'a> {
        GridRefIter { grid: self, i: 0, j: 0 }
    }
}

struct GridRefIter<'a> {
    grid: &'a Grid,
    i: usize,
    j: usize,
}

impl<'a> Iterator for GridRefIter<'a> {
    type Item = (u32, u32);

    fn next(&mut self) -> Option<(u32, u32)> {
        if self.i >= self.grid.x_coords.len() {
            self.i = 0;
            self.j += 1;
            if self.j >= self.grid.y_coords.len() {
                return None;
            }
        }
        let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));
        self.i += 1;
        res
    }
}

struct GridIter {
    grid: Grid,
    i: usize,
    j: usize,
}

impl Iterator for GridIter {
    type Item = (u32, u32);

    fn next(&mut self) -> Option<(u32, u32)> {
        if self.i >= self.grid.x_coords.len() {
            self.i = 0;
            self.j += 1;
            if self.j >= self.grid.y_coords.len() {
                return None;
            }
        }
        let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));
        self.i += 1;
        res
    }
}

fn main() {
    let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] };
    for (x, y) in &grid {
        println!("point = {x}, {y}");
    }
    for (x, y) in &grid {
        println!("point = {x}, {y}");
    }
}

对于标准库类型也可能出现同样的问题:for e in some_vector 会获取 some_vector 的所有权,并遍历该向量中的拥有所有权的元素。改为使用 for e in &some_vector,以便遍历 some_vector 元素的引用。