前言
一般来说,Rust 里定义了一个结构 struct,是不会给他实现 Iterator
trait 的,如果希望能够支持 for in
遍历,需要实现 Iterator
trait。
在 Rust 中通常会定义一个 iter()
方法,转换成另一个实现了 Iterator
trait 的结构。
这里我们简单实现一个,并讲讲这个过程中 Rust 里特有的语法,概念等。
Implementation
简单定义一个 struct,里面存放着 0 到 9 这是个数字,看起来写得很脱裤子放屁,用来演示而已:
struct Nums {
values: [usize; 10],
}
impl Nums {
pub fn new() -> Self {
let mut arr = [0; 10];
let mut n = 0;
for v in arr.iter_mut() {
*v = n;
n += 1;
}
Self { values: arr }
}
}
这个 Nums 是无法在 for in
里遍历的:
fn main() {
let n = Nums::new();
for v in n {
println!("{}", v);
}
}
编译器会抱怨:
`Nums` is not an iterator
the trait `Iterator` is not implemented for `Nums`
required for `Nums` to implement `IntoIterator`
根据提示需要实现 Iterator
trait,按照我们一开始说的,倾向于调用 iter()
方法转换成另一个实现了 Iterator
的结构:
impl Nums {
pub fn iter(&self) -> NumsIter {
NumsIter { cur: 0, num: self }
}
}
这里 iter
方法返回了一个 NumsIter 结构,它的定义就像这样:
struct NumsIter<'a> {
cur: usize,
num: &'a Nums,
}
为什么不直接实现?
在 Nums 直接实现 Iterator
不行吗?
可以,但是遍历的功能我们会保留一个变量,用来记录上次的值,如果直接在 Nums 上实现,那么就需要多这样一个变量:
struct Nums {
cur: usize, // 用于记录上次遍历的值
values: [usize; 10],
}
可是这个变量 cur
只有在遍历的时候在用得到对吧,不遍历的时候根本用不到,我不想在我不需要的时候有这个变量来污染我的结构,还是放在 NumsIter 里吧。
生命周期
在 NumsIter 结构里会看到这个玩意儿:<'a>,这坨玩意儿是 Rust 特有(咬牙切齿)的生命周期标注。
因为 Rust 里所有权的存在,当一个变量赋予给了另一个变量,只要没有实现 Copy
trait,先前变量的所有权就转移到了别的变量里面了,先前变量就无法在使用:
let a = String::new();
let b = a;
println!("{}", a);
// 编译器抱怨 borrow of moved value: `a`
// value borrowed here after move
这里 'a' 赋值给了 'b','a' 的所有权就给了 'b',变量 'a' 不能再使用。
我们的 NumsIter 里需要遍历 Nums 里的值,就需要拿到 Nums 的数据,可是如果将 Nums 的所有权给 NumsIter 的话,遍历完了后 Nums 就无法使用了,也是非常不便的。
所幸的是不需要持有 Nums 的所有权,而是临时借用一下 Nums。
在 NumsIter 的属性定义里的这个属性:num: &'a Nums
,&
符号代表借用一下。你可以类比一下 C 里面的指针,或者引用。
可是这样又带来了另一个问题:我 NumsIter 借用了 Nums,可是如果我 NumsIter 还在借用的时候 Nums 释放了怎么办?悬垂指针很熟悉吧~
这就需要生命周期检查了!也就是 num: &'a Nums
里的 'a
。
<'a>
由单引号标注在左侧的泛型是生命周期标注,就是一个用于生命周期标注的泛型,就跟普通的泛型一样,使用前需要先声明:struct NumsIter<'a> {},使用时标注在变量的借用符号 &
左侧:num: &'a Nums
,表示变量 num
借用的对象所存在的时间不得少于 'a
所标注的时间。
怎么理解呢?
需要整体来看,NumsIter 的定义:
struct NumsIter<'a> {
cur: usize,
num: &'a Nums,
}
NumsIter 定义了生命周期标注 'a
,意味着 'a
生命周期和 NumsIter 一样长:
fn main() {
let t = NumsIter {..., ...};
}
那么就存在一个生命周期 'a
和 变量 t
同声同死:
fn main() {
'a --
let t = NumsIter {..., ...}; |
--
}
然后属性里 num: &'a Nums
表示借用的 Nums 存活的时间必须不小于(outlive) 'a
。所以我们这样写是合法的:
let v = Nums::new();
let t = NumsIter {0, v};
他们的生命周期是这样的:
fn main() {
let v = Nums::new(); v -------
'a -- |
let t = NumsIter {0, v}; | |
-- |
-----
}
变量 v
存活的时间超过 NumsIter 定义的生命周期 'a
,编译通过。这样一来,就可以保证在 NumsIter 借用 Nums 的时间内 Nums 都是存在的。
实现 Iterator
给我们的 NumsIter 实现 Iterator
trait 以支持遍历。
impl<'a> Iterator for NumsIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
todo!();
}
}
实现 trait 的语法:impl trait for struct {}
我们又看到了一个声明周期标注 'a
,还是一个意思,给 NumsIter 实现 trait 需要生命周期标注,就跟泛型一样。
这个 type Item = usize;
是什么?关联类型。额,其实就是泛型。
那为什么写成关联类型,因为如果泛型太多的话写在尖括号里写不下。<T, J, K, .......>
,写成关联类型就好看多了。
在 Iterator 里面的关联类型 Item 指示 next 方法里的返回类型。 实现后是这样的,非常古典的写法。
impl<'a> Iterator for NumsIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.num.values.len() {
let res = Some(self.num.values[self.cur]);
self.cur += 1;
res
} else {
None
}
}
}
完整代码
实现完了后就可以在 for in
里调用了:
fn main() {
let n = Nums::new();
for v in n.iter() {
println!("{}", v);
}
}
完整代码:
fn main() {
let n = Nums::new();
for v in n.iter() {
println!("{}", v);
}
}
struct Nums {
values: [usize; 10],
}
impl Nums {
pub fn new() -> Self {
let mut arr = [0; 10];
let mut n = 0;
for v in arr.iter_mut() {
*v = n;
n += 1;
}
Self { values: arr }
}
pub fn iter(&self) -> NumsIter {
NumsIter { cur: 0, num: self }
}
}
struct NumsIter<'a> {
cur: usize,
num: &'a Nums,
}
impl<'a> Iterator for NumsIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.num.values.len() {
let res = Some(self.num.values[self.cur]);
self.cur += 1;
res
} else {
None
}
}
}
Con
嘿!你还别说,小小一段 Rust 需要了解的东西还挺多的。
作者详细介绍了如何在Rust中实现自定义迭代器。文章首先指出,直接使用
for v in n
会导致编译失败,因为n
没有实现IntoIterator
trait。为了解决这个问题,作者选择通过创建一个自定义的迭代器结构来满足需求。接下来,作者解释了为什么需要设计迭代器的状态。迭代器通常需要跟踪当前位置以确保能够正确遍历所有元素。文章中使用了一个变量
cur
作为索引,并在每次调用next()
方法时递增该索引,这与常见的数组或集合的迭代方式类似。然后,作者详细讲解了生命周期管理的重要性,特别是在Rust中如何通过生命周期标注来保证借用关系的安全性。这部分内容对于理解Rust的所有权和借用规则至关重要。正确的生命周期管理可以避免许多潜在的内存安全问题,并确保程序的稳定性。
最后,文章提供了完整的代码示例,展示了从定义结构体
Nums
、实现new()
方法到自定义迭代器NumsIter
的整个过程。这让我认识到,在Rust中实现自定义迭代虽然需要一些额外的工作,但通过合理的设计和实现,完全可以达到预期的效果。总的来说,这篇文章深入浅出地讲解了在Rust中如何实现一个可迭代的结构,涵盖了从基本概念到实际编码的全过程。对于理解Rust的迭代器机制非常有帮助,并且为开发者提供了清晰的指导,如何在需要时创建自定义的迭代器以满足特定需求。
首先,我想对您的博客表示赞赏。您对Rust语言中的Iterator的实现进行了深入且详细的解析,对于初学者来说,这是一篇极具启发性的文章。您使用了简单易懂的语言来解释复杂的概念,如生命周期标注,这对于理解Rust的所有权和借用机制非常有帮助。
您的文章中,我特别赞赏的是您对代码示例的使用。您的示例代码既具有实际意义,又能清楚地展示如何在Rust中实现Iterator。通过这些示例,读者可以更好地理解Rust中的语法和概念。
然而,我认为这篇文章还有一些可以改进的地方。首先,对于一些初学者来说,可能对Rust的语法和概念并不熟悉,因此在解释这些概念时,可能需要更详细的解释和更多的示例。其次,我注意到在文章中,您对一些概念的解释可能有些过于简略,例如,您在解释生命周期标注时,可能需要更详细地解释其作用和使用场景。
总的来说,这是一篇非常有价值的文章,我期待您能在未来的文章中,继续分享您对Rust的理解和经验。