手把手教你实现 Rust 迭代器

在完成前面三篇关于 Rust 迭代器的深入学习之后,本篇文章我们尝试自己写一下 Rust 迭代器,即给我们自己的数据加上迭代器的功能。

如果有读者想复习之前的文章:

开始之前

Rust 原生的 Vec 以及数组,已经支持迭代器相关功能了:

rust 复制代码
let mut a = vec![1, 2, 3];

for i in &a {
    println!("{}", i);
}

a.iter_mut().for_each(|i| {
    *i = 100;
});

println!("{:?}", a);

// output
// 1
// 2
// 3
// [100, 100, 100]

上述代码如果将 vec![1, 2, 3] 改成 [1, 2, 3],效果是一样的。

如果你的数据结构有基于 Vec 或者数组去实现迭代器,其实可以直接用 Vec 或者数组的迭代器,完全没有必要自己实现。

但是为了让我们更加了解迭代器的内部原理以及相关的实现细节,该篇文章我们尝试为自己的数据实现一个迭代器。

首先,你需要一个简单的类:

rust 复制代码
#[derive(Debug)]
struct Rect {
    a: f32,
    b: f32,
    c: f32,
    d: f32,
}

为类实现迭代器

深入 Rust 迭代器(上) 文中已经知道,如果我们要为自己的数据实现一个迭代器,就需要实现 Iterator trait

rust 复制代码
impl Iterator for Rect {
    type Item = f32;

    fn next(&mut self) -> Option<Self::Item> {
        //...
    }
}

trait 需要实现的函数非常简单 ------ next 即可。

next 返回一个 Option,如果迭代器结束,直接返回 None

为了能够实现迭代器功能,我们需要对 Rect 进行一点改造,添加一个迭代器下标:

rust 复制代码
#[derive(Debug)]
struct Rect {
    a: f32,
    b: f32,
    c: f32,
    d: f32,

    iter_index:u8, // 用于计数当前迭代的位置
}

然后,为 Rect 实现迭代器:

rust 复制代码
impl Iterator for Rect {
    type Item = f32;

    fn next(&mut self) -> Option<Self::Item> {
        let next = match self.iter_index {
            0 => {
                Some(self.a)
            }
            1 => {
                Some(self.b)
            }
            2 => {
                Some(self.c)
            }
            3 => {
                Some(self.d)
            }
            _ => None,
        };

        self.iter_index += 1;

        next
    }
}

现在知道为什么 next 需要的入参是 &mut self 了吧!

每次调用 next 的时候,我们先返回当前的值,然后对迭代的下标 +1

如果迭代结束了,则返回 None

rust 复制代码
let rect = Rect {
    a: 1.0,
    b: 2.0,
    c: 3.0,
    d: 4.0,
    iter_index: 0,
};

for i in rect {
    println!("{}", i);
}

// output
// 1
// 2
// 3
// 4

OK,很简单。

但是这样写,会有两个严重问题:

  • iter_index 对迭代的影响很大,如果这个值设置错误,迭代就不能从头开始。
  • 无法再次使用迭代,即不能再次使用 for i in rect

我们来看第一个问题,如果我们给 iter_index3。结果就会变成:

txt 复制代码
4

对,只会输出一个 4,因为我们的 next 实现,是基于 iter_index 做的。

第二个问题,当我们再次使用 rect 进行迭代的时候:

会得到这样一个错误。

原因是,for i in rect 是 Rust 提供的语法糖,实际上那段代码会被编译成如下的代码:

rust 复制代码
let mut iter = IntoIterator::into_iter(collection);
loop {
    match iter.next() {
        Some(i) => {
            println!("{}", i);
        }
        None => break,
    }
}

查看 IntoIterator::into_iter 的源码,我们发现:

rust 复制代码
#[inline]
fn into_iter(self) -> I {
    self
}

IntoIterator::into_iter 会获取入参的所有权,这也就是为什么不能写两次 for i in rect 的原因。

为了解决上面两个问题,我们看看正确的迭代器是如何实现的。

IntoIterator

Rust 提供了一个 trait ------ IntoIterator,它的作用就是把一个"可迭代的东西"(比如容器)转换成一个"迭代器"

这里,我们的 Rect 就是一个可迭代的数据,通过实现 IntoIterator 就可以变成一个迭代器了。

rust 复制代码
#[derive(Debug)]
struct Rect {
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    // 这里去掉了 iter_index
}

struct RectIter {
    data: Rect,
    iter_index: u8,
}

impl Iterator for RectIter {
    type Item = f32;

    fn next(&mut self) -> Option<Self::Item> {
        match self.iter_index {
            0 => {
                self.iter_index += 1;
                Some(self.data.a)
            }
            1 => {
                self.iter_index += 1;
                Some(self.data.b)
            }
            2 => {
                self.iter_index += 1;
                Some(self.data.c)
            }
            3 => {
                self.iter_index += 1;
                Some(self.data.d)
            }
            _ => None,
        }
    }
}

impl IntoIterator for Rect {
    type Item = f32;
    type IntoIter = RectIter;

    fn into_iter(self) -> Self::IntoIter {
        RectIter {
            data: self,
            iter_index: 0,
        }
    }
}

读者会发现,我们对 Rect 实现了 IntoIterator,该 trait 要求返回一个 IntoIter 类型,IntoIter 实际上是 type IntoIter: Iterator<Item = Self::Item>;,也就是一个迭代器。

此处,我们定义了一个新的迭代器类型 RectIter 并实现了 Iterator,最后 IntoIterator 中的 into_iter(self) 返回 RectIter

好的,现在我们来看看效果:

rust 复制代码
let rect = Rect {
    a: 1.0,
    b: 2.0,
    c: 3.0,
    d: 4.0,
};

for i in rect {
    println!("{}", i);
}

// output
// 1
// 2
// 3
// 4

我们不用再在 Rect 中定义迭代下标了,当使用迭代器的时候,RectIter 中的迭代下标,都会设置为 0 开始。

但是它依然没有解决重复使用的问题,也就是我们不能再次迭代 Rect

迭代 &

我们要做的事情实际上很简单,就是为 &Rect 提供一个迭代器,这样我们就可以针对 &Rect 进行迭代了。

rust 复制代码
// 为 &Rect 创建一个迭代器结构体(借用型)
struct RectRefIterator<'a> {
    rect: &'a Rect,
    index: usize,
}

// 为 RectRefIterator 实现 Iterator trait
impl<'a> Iterator for RectRefIterator<'a> {
    type Item = f32;

    fn next(&mut self) -> Option<Self::Item> {
        let result = match self.index {
            0 => Some(self.rect.a),
            1 => Some(self.rect.b),
            2 => Some(self.rect.c),
            3 => Some(self.rect.d),
            _ => None,
        };
        self.index += 1;
        result
    }
}

// 为 &Rect 实现 IntoIterator trait(借用型)
impl<'a> IntoIterator for &'a Rect {
    type Item = f32;
    type IntoIter = RectRefIterator<'a>;

    fn into_iter(self) -> Self::IntoIter {
        RectRefIterator {
            rect: self,
            index: 0,
        }
    }
}

我们直接定义让 &Rect 实现 IntoIterator trait,这里的写法比较长:impl<'a> IntoIterator for &'a Rect

'a 是一个生命周期标注,如果你还不熟悉生命周期标注,可以先跳过,我们可以简单的理解为如果你有引用类型,需要帮助编译器来确定这个引用能存活多长时间。

此时,返回的 IntoIter 类型我们需要重新定义一个 RectRefIterator<'a>,同样,它会持有一个 &'a Rect,即 &Rect 类型。

这样,我们就可以多次遍历了。

rust 复制代码
let rect = Rect {
    a: 1.0,
    b: 2.0,
    c: 3.0,
    d: 4.0,
};

for i in &rect { // 注意这里是 &rect
    println!("{}", i);
}

for i in &rect {
    println!("{}", i);
}

for i in &rect {
    println!("{}", i);
}

如果你想消耗掉 rectfor i in rect 即可。

那么,如果我想支持 &mut 呢?

迭代 &mut

for i in &mut rect 一般这种迭代形式,是说我想在迭代的时候更改元素,例如下面这个在迭代的时候更改数组里面的值:

rust 复制代码
for i in &mut a {
    *i = 100;
}

好的,这应该是所有迭代器中,编写最麻烦的一个了。

我们尝试,按照上面那个写法,写一个迭代器试试:

rust 复制代码
struct RectMutRefIterator<'a> {
    rect: &'a mut Rect,
    index: usize,
}

impl<'a> Iterator for RectMutRefIterator<'a> {
    type Item = &'a mut f32;

    fn next(&mut self) -> Option<Self::Item> {

        let index = self.index;
        self.index += 1;

        match index {
            0 => Some(&mut self.rect.a),
            1 => Some(&mut self.rect.b),
            2 => Some(&mut self.rect.c),
            3 => Some(&mut self.rect.d),
            _ => None,
        }
    }
}

其实就是把 &Rect 变成了 &mut Rect

但是这段代码是无法通过编译的,你会得到这样一段提示

这里简单解释一下,fn next(&mut self) -> Option<Self::Item> 实际上是 fn next(&mut self) -> Option<&'a mut f32>,这个函数需要返回一个带 'a 生命周期的引用,但是 Rust 编译器不知道这个 'a 从哪儿来的,Rust 函数的生命周期标注,必须要有一个来源。

那么如果我们改成 fn next(&'a mut self) ?

也不行,因为 impl<'a> Iterator 的函数定义不是这样的,此时,似乎陷入一个僵局了···

由于这个地方过于复杂,我们不再深入探讨,我先给出用正确答案的一种:

rust 复制代码
// ----------------------------------------------------
// 1. 自定义迭代器结构体
// ----------------------------------------------------
pub struct RectMutIterator<'a> {
    // 指向 Rect 内部当前 f32 元素的原始可变指针。
    ptr: *mut f32,
    // 剩余的元素数量。
    len: usize,
    // 生命周期标记:将迭代器的生命周期 'a 绑定到 Rect 的 &mut 借用上。
    _marker: PhantomData<&'a mut f32>,
}

// ----------------------------------------------------
// 2. 针对 &mut Rect 实现 IntoIterator
// ----------------------------------------------------
impl <'a> IntoIterator for &'a mut Rect {
    type Item = &'a mut f32;
    type IntoIter = RectMutIterator<'a>;

    fn into_iter(self) -> Self::IntoIter {
        let ptr_f32 = self as *mut Rect as *mut f32;

        RectMutIterator {
            ptr: ptr_f32,
            len: 4, // 字段总数
            _marker: PhantomData,
        }
    }
}

// ----------------------------------------------------
// 3. 迭代器的实现
// ----------------------------------------------------
impl<'a> Iterator for RectMutIterator<'a> {
    type Item = &'a mut f32; // 返回 f32 的可变引用

    fn next(&mut self) -> Option<Self::Item> {
        // 1. 检查是否迭代完毕
        if self.len == 0 {
            return None;
        }

        // 2. 减少剩余元素计数
        self.len -= 1;

        // 3. 核心 unsafe 逻辑
        unsafe {
            // 获取当前元素的指针
            let current_ptr = self.ptr;

            // 将内部指针移动到下一个元素
            // offset(1) 是一个 safe 抽象,但它内部执行的是 unsafe 指针算术
            // 它确保 `self.ptr` 现在指向下一个 f32 (即下一个字段)
            self.ptr = self.ptr.offset(1);

            // 4. 将当前的原始指针转换回安全的 &mut f32 引用
            // 这是安全的,因为我们通过 len 和 ptr 的管理,
            // 确保了当前元素是独占的,且生命周期被正确绑定。
            Some(&mut *current_ptr)
        }
    }
}

我们测试一下看看:

rust 复制代码
let mut rect = Rect {
     a: 1.0,
     b: 2.0,
     c: 3.0,
     d: 4.0,
 };

for i in &mut rect {
     *i += 10.0;
 }

println!("{:?}", rect);

// output
// Rect { a: 11.0, b: 12.0, c: 13.0, d: 14.0 }

结果是正确的。

好的,到此,几种基本的迭代器类型我们已经实现完毕了。除了 &mut 比较复杂以外,其他两种还算是轻松。

Rust 的一些习惯

实际上,Rust 中针对迭代器,还有一些习惯,我们在 深入 Rust 迭代器(上) 提到 Rust 迭代器中,有三种等价情况:

  • for i in &vec 等价于 for i in vec.iter()
  • for i in &mut 等价于 for i in vec.iter_mut()
  • for i in vec 等价于 for i in vec.into_iter()

所以,我们给 Rect 添加一下这三个方法:

rust 复制代码
impl Rect {
    fn iter_mut(&mut self) -> RectMutIterator {
        RectMutIterator {
            ptr: self as *mut Rect as *mut f32,
            len: 4,
            _marker: PhantomData,
        }
    }

    fn iter(&self) -> RectRefIterator {
        RectRefIterator {
            rect: self,
            index: 0,
        }
    }

    fn into_iter(self) -> RectIter {
        RectIter {
            data: self,
            iter_index: 0,
        }
    }
}

这样,我们就可以使用迭代器适配器方法和管道了:

rust 复制代码
let mut rect = Rect {
    a: 1.0,
    b: 2.0,
    c: 3.0,
    d: 4.0,
};

rect.iter_mut().for_each(|i| {
    *i += 10.0;
});

rect.iter().for_each(|a| print!("{:?} ", a));

println!();

rect.into_iter().for_each(|a| print!("{:?} ", a));

println!();

// output
// 11.0 12.0 13.0 14.0
// 11.0 12.0 13.0 14.0

倒序

深入 Rust 迭代器(下) 中,我们最后提到一个 rev(),也就是迭代器倒序。这里我们也实现一下,这样你就能明白那篇文章中,为什么输出结果是那样的了。

我们仅针对 &Rect 实现倒序。

为了实现倒序,我们需要给 RectRefIterator 添加一个 end 字段,该字段和 index 其实效果是一样的,用于标识当前应该返回哪个数据。

rust 复制代码
struct RectRefIterator<'a> {
    rect: &'a Rect,
    index: usize,
    end: usize,
}

接下来在所有用到 RectRefIterator 的地方,设置 end4

rust 复制代码
RectRefIterator {
    rect: self,
    index: 0,
    end: 4,
}

为了能够让迭代器支持 rev(),我们需要实现一个 DoubleEndedIteratortrait,该 trait 只需要实现 fn next_back(&mut self) 即可,即倒着返回数据。

rust 复制代码
impl <'a> DoubleEndedIterator for RectRefIterator<'a> {
    fn next_back(&mut self) -> Option<Self::Item> {
        let result = match self.end {
            1 => Some(self.rect.a),
            2 => Some(self.rect.b),
            3 => Some(self.rect.c),
            4 => Some(self.rect.d),
            _ => return None,
        };
        self.end -= 1;
        result
    }
}

好,万事俱备,我们测试一下:

rust 复制代码
let mut rect = Rect {
    a: 1.0,
    b: 2.0,
    c: 3.0,
    d: 4.0,
};

rect.iter().rev().for_each(|a| print!("{:?} ", a));

// output
// 4.0 3.0 2.0 1.0

完美。

总结

好的,关于 Rust 迭代器的这个系列,终于迎来了完结。

我们在最后这篇文章中,给自己的数据实现了迭代器功能。

通过我们手动编写迭代器,我们不仅学会了如何实现迭代器,也加深了对 Rust 中数据使用的理解,你会发现,Rust 始终围绕所有权借用可变借用来实现数据的访问。

这种设计不是限制,而是一种引导------它迫使我们在编写代码的时候就思考:

  • 谁拥有数据?
  • 谁在读取或修改它?
  • 生命周期是否安全?

正是这些看似"严格"的规则,让 Rust 能在不依赖垃圾回收的情况下,既保证内存安全,又实现零成本抽象。而迭代器,正是这一哲学的完美体现:它把控制权交给开发者,又用类型系统默默守护着每一份引用的安全。

现在,当你再看到 for x in vec 时,或许不再只觉得它简洁优雅,更能体会到背后那套精密、可靠、充满智慧的机制在默默运转。

感谢你一路走到这里。愿你在 Rust 的旅程中,继续享受这种"被约束的自由"。

相关推荐
q***49863 小时前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
王燕龙(大卫)12 小时前
rust:所有权
开发语言·rust
Source.Liu12 小时前
【Chrono库】国际化本地化系统架构解析:条件编译的精妙设计(locales.rs)
rust·time
全栈陈序员1 天前
基于Rust 实现的豆瓣电影 Top250 爬虫项目
开发语言·爬虫·rust
百锦再1 天前
第17章 模式与匹配
开发语言·后端·python·rust·django·内存·抽象
JosieBook1 天前
【Rust】基于Rust + WebAssembly;实现人机记忆井字棋游戏(人机对战)
游戏·rust·wasm
万事可爱^1 天前
GitHub爆火开源项目——RustScan深度拆解
c语言·开发语言·rust·开源·github·rustscan
受之以蒙1 天前
具身智能的“任督二脉”:用 Rust ndarray 打通数据闭环的最后一公里
人工智能·笔记·rust
有梦想的攻城狮1 天前
初识Rust语言
java·开发语言·rust