searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Rust入门(九) —— 迭代器

2024-05-27 01:59:21
33
0

从for循环遍历数组说起

在c语言时代(这个说法不妥,c语言依然没有被时代抛弃),如果遍历一个数组,我们通常会用for循环,代码类似于这样:

int arr[] = {1, 2, 3, 4, 5}; 
int i;
for (i = 0; i < 5; i++ ) {
    printf("%d\n", arr[i]);
}

c语言的遍历核心是基于索引:起始位置,结束条件,索引变更方式。通过控制索引,找到集合中的数据元素。

 

在golang中,遍历同样的数组,一般有两种方式:

其一,按索引遍历,这种方式和c语言是一致的:

arr := []int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
    fmt.Println(arr[i])
}

其二,通range关键字进行“迭代”:

arr := []int{1, 2, 3, 4, 5}
for i, v := range arr {
    fmt.Println("Index:", i, "Value:", v)
}

golang中,并没有语言层面的“迭代”这个概念,可以理解为range 是一个语法糖,将一些可以索引化的数据类型(并且由golang 语言层面进行了索引化支持),自动进行索引化转换(设置起始位置,结束条件,索引变更方式),然后可以应用于for循环。

但golang的 for range支持了更丰富的数据类型:数组,切片,映射,通道;从整体上看,golang 的for  range呈现了一种迭代化获取数据的方式:

在一个数据集合中,依次获取集合中的数据元素,而不用关注底层数据存储形式、以及数据扫描、获取方式!

rust 则是通过引入迭代器,将上述“迭代化获取数据”的方式作为一种通用的数据遍历方式!

迭代器允许我们迭代化访问一个连续的集合,例如数组、动态数组 VecHashMap 等,以及任何实现了迭代器特性的自定义数据集合;在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。

 

在rust中,遍历一个数组类似这样:

let  vec = vec![1, 2, 3, 4, 5];
for i in vec {
       println!("{}", i); 
}

看上去,这端代码和golang 的for range 几乎一致(只不过是关键字 换成了in);但实际上它们之间存在本质差别:golang 的for range 本质还是通过索引获取数据(所以可以拿到index),而rust 的for in是通过迭代器的迭代方法(next方法)获取数据!

为了理解这点,我们还得从rust 的 Iterator 特性说起......

Iterator (迭代器)

Iterator 是rust标准库中定义的一个trait,通常称之为“迭代器”。任何实现了该trait 的结构体,都可以用 for in 的方式进行迭代化数据访问。Iterator trait 中有两个关键的定义,其一是 type Item,表示该迭代器返回的数据类型;其二是 next方法,即迭代方法(通过循序调用next方法,遍历集合中的数据):

pub trait Iterator {
    /// The type of the elements being iterated over.
    type Item;
   /// Advances the iterator and returns the next value.
   /// Returns None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may 
   /// or may not eventually start returning Some(Item) again at some point.
    fn next(&mut self) -> Option<Self::Item>;

  ... // 其他对集合操作的函数接口
}

 作为示例,我们可以写一个自己的简易迭代器:

struct Revolver {
    bullets: u16,
}

impl Revolver {
    fn new() -> Self {
        Revolver { bullets: 6 }
    }
    fn reload(&mut self) {
        self.bullets = 6;
    }
}

impl Iterator for Revolver {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.bullets > 0 {
            self.bullets -= 1;
            Some("Bang!".to_string())
        } else {
            None
        }
    }
}

#[test]
fn fire_all_bullets() {
     let  revolver = Revolver::new();
     for fired in revolver {
          println!("{}", fired);
     }
}

见代码片段,我们实现了一把左轮手枪,默认填6发子弹; 用for in 测试连续射击(fire_all_bullets),执行效果:

事实上,fire_all_bullets还有一个更直白的写法:

#[test]
fn fire_all_bullets() {
        let mut revolver = Revolver::new();
        loop {
            // 扣动扳机
            let fired = revolver.next();

            match fired {
                Some(bullet) => {
                    // 子弹发射
                    println!("{}", bullet)
                }
                // 没有子弹了,射击结束
                None => break,
            }
        }
}

上述代码和用for in 实现效果一样:Bang!Bang!Bang!Bang!Bang!Bang!

注意:当使用next() 手动遍历时,迭代器变量要设置为可变,这是因为迭代过程中会修改迭代器内部的数据!

事实上,loop展开的迭代器调用模式,才是迭代器真实执行的模型;for in 可以看做是编译器的语法糖,在编译期间将for in 迭代器展开为上述loop 的模型;只不过处理过程中,还会涉及一些数据克隆,引用转换,可变引用转换等,以方便使用;前一个测试中,for in 的revolver 没有加mut 也可以执行,这是因为for in 展开时,自己做了一次转换!

 

IntoIterator

第一章有个举例,for in 遍历数组:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr {
       println!("{}", i); 
}

按第二章的理解,for in 可以 直接作用于Vec,那么Vec应该实现了 Iterator?即Vec本身也是迭代器?

查看Vec实现并未发行Vec 实现了Iterator,相反它实现了IntoIterator 这个 trait。

功能如其名,IntoIterator 这个特性就是将实现了这个trait的数据转化为一个迭代器。

pub trait IntoIterator {
    /// The type of the elements being iterated over.
    type Item;

    /// Which kind of iterator are we turning this into?
    type IntoIter: Iterator<Item = Self::Item>;

    /// Creates an iterator from a value.
    fn into_iter(self) -> Self::IntoIter;
}

该trait主要包括三个成员: Item指定迭代器返回的元素;IntoIter 指定迭代器类型; into_iter实现由当前数据类型转化为指定迭代器的方法!

下面,我们用一个实例来演示一下IntoIterator 及Iterator 的关系和使用。

 

实战

考虑这样一个需求:对一个字符串,以空格为分割的方式,遍历获取每个单词。

直观的,要实现对一个字符串按空格进行逐个单词的迭代分割,我们需要引入一个位置标记(position),记录当前已经完成了单词分割的位置;当下一次请求单词时,从剩余的内容中找到下一个空格(或者直到内容末尾),将这部分内容作为分割的单词返回;同时,更新位置标记到当前返回的位置;最后,当位置标记到达原始字符串末尾时,处理完成,后续请求返回为None。

按照上面的思路,给出编码如下:

首先,自定义字符串数据结构:

// 定义MyString结构体
struct MyString {
    content: String,
}

通常,我们需要对现有数据进行额外处理时,为了不破坏原有的数据结构和实现,通过wapper包装是一个不错的选择。

其次,定义MyString的迭代器数据结构(根据上面的分析,需要一个位置标记来记录处理过程):

// 定义MyStringIterator结构体
struct MyStringIterator {
    content: String,
    position: usize,
}

 第三,为MyStringIterator 实现Iterator特性,使之成为迭代器:

impl Iterator for MyStringIterator {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let refx = self.content.as_str();
        let bytes = refx.as_bytes();
        while self.position < refx.len() && bytes[self.position] == b' ' {
            self.position += 1;
        }

        if self.position < refx.len() {
            let rest = &refx[self.position..];
            let idx = rest.find(' ').unwrap_or(rest.len());
            let result = rest[0..idx].to_string();
            self.position += idx;
            Some(result)
        } else {
            None
        }
    }
}

这里,我们指定返回迭代的元素为String,即每个单词以String类型返回;next实现中,会自动跳过连续的空格,通过 &str.find接口找到下一个空格的位置(如果找不到,则直到末尾),并进行字符切片截取,再转化为Some(String)返回。

第四,为MyString 实现IntoIterator 特性,使其可以转换为迭代器,方便对内容进行迭代处理:

impl IntoIterator for MyString {
    type Item = String;
    type IntoIter = MyStringIterator;

    fn into_iter(self) -> Self::IntoIter {
        MyStringIterator {
            content: self.content,
            position: 0,
        }
    }
}

需要注意的是,实现IntoIterator时,需要指定元素类型和迭代器类型,并且所指定的元素类型和迭代器要求的元素类型相匹配。

into_inter中,接受的self 是MyString的实例,并且获得其所有权;同时,将MyString中content变量绑定到 迭代器的content上;初始化位置为0.

第五, 测试应用

 #[test]
    fn test_my_string_iterator() {
        let my_string = MyString {
            content: "My name is LiLei".to_string(),
        };
        let mut iter = my_string.into_iter();
        assert_eq!("My", iter.next().unwrap());
        assert_eq!("name", iter.next().unwrap());
        assert_eq!("is", iter.next().unwrap());
        assert_eq!("LiLei", iter.next().unwrap());

        let nstr = MyString {
            content: "What's your name?".to_string(),
        };
        for word in nstr {
            println!("{}", word);
        }
    }

执行效果:

bingo, 测试通过。手动迭代assert,for 循环打印符合预期按空格分割的单词进行迭代输出。

 

再说for in Vec

前面说到了 Vec并没有实现Iterator,那么Vec在for in 结构里是怎么执行的呢?

前面也说到,for in 结构在rust 中是一种语法糖,它自己会进行类型推导:for in 结构后变量是迭代器时,对迭代器进行循环展开;当for in 结构后的变量非迭代器时,编译器会推断其必须可以进行迭代器转换,即它必须有一个 IntoIterator 实现,并尝试调用其into_iter方法获取迭代器(如果没有实现则会报错)。

所以,开始示例的for in 直接作用于数组:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr {
       println!("{}", i); 
}

等效于:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr.into_iter {
       println!("{}", i); 
}

值得注意的是:

into_iter 进行迭代器转换时,会移走原始数据的所有权,导致原始数据不再可用!

如下测试代码:

#[test]
fn test_iter_vec() {
        let values = vec![1, 2, 3];
        for v in values {
            println!("{}", v)
        }
        println!("{:?}", values);
}

运行代码会报错:

错误提示说得明白,values 在再调用into_iter() 时被移交了所有权!

iter, iter_mut

在实际开发过程中,迭代器在数据处理上提供了很大的便利性;但是into_iter会拿走所有权会限制该模式在很多场景的使用。因此,很多数据类型在实现原始数据到迭代器的转化时,还提供了其他的转化方式。比如Vec还提供了 iter() 返回一个以不可变借用的方式遍历原数据的迭代器;iter_mut() 返回一个以可变借用的方式遍历原数据的迭代器。

以下是into_iter()iter()iter_mut()的一个对比测试, 大家可以实际跑一下,理解输出:

#[test]
fn test_iter_vec() {
        let values = vec![1, 2, 3];
        for v in values.into_iter() {
            println!("{}", v)
        }
        // 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
        // println!("{:?}",values);

        let values = vec![1, 2, 3];
        for v in values.iter() {
            println!("{}", v)
        }
        // 不会报错,因为 values_iter 只是借用了 values 中的元素
        println!("{:?}", values);

        let mut values = vec![1, 2, 3];
        for v in values.iter_mut() {
            *v = *v * 2;
        }
        // 输出[0, 4, 6]
        println!("{:?}", values);
}

注意:iter,iter_mut 不是 IntoIterator 的标准接口,用在for循环中时,必须显示调用!

当然,你也可以定义自己的转化函数,定义对原始数据的处理方式;但为了增加代码可读性,强烈建议遵循rust社区的一些通用编程实践!

最后

迭代器 Iterator中还有很多实用接口,比如map(),filter(), zip(),  count()等等。熟练掌握这些接口的功能和使用方法,能够让你在数据处理时更加游刃有余。这部分内容,就留给大家自行学习补充吧。

 

0条评论
0 / 1000
huskar
18文章数
2粉丝数
huskar
18 文章 | 2 粉丝
原创

Rust入门(九) —— 迭代器

2024-05-27 01:59:21
33
0

从for循环遍历数组说起

在c语言时代(这个说法不妥,c语言依然没有被时代抛弃),如果遍历一个数组,我们通常会用for循环,代码类似于这样:

int arr[] = {1, 2, 3, 4, 5}; 
int i;
for (i = 0; i < 5; i++ ) {
    printf("%d\n", arr[i]);
}

c语言的遍历核心是基于索引:起始位置,结束条件,索引变更方式。通过控制索引,找到集合中的数据元素。

 

在golang中,遍历同样的数组,一般有两种方式:

其一,按索引遍历,这种方式和c语言是一致的:

arr := []int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
    fmt.Println(arr[i])
}

其二,通range关键字进行“迭代”:

arr := []int{1, 2, 3, 4, 5}
for i, v := range arr {
    fmt.Println("Index:", i, "Value:", v)
}

golang中,并没有语言层面的“迭代”这个概念,可以理解为range 是一个语法糖,将一些可以索引化的数据类型(并且由golang 语言层面进行了索引化支持),自动进行索引化转换(设置起始位置,结束条件,索引变更方式),然后可以应用于for循环。

但golang的 for range支持了更丰富的数据类型:数组,切片,映射,通道;从整体上看,golang 的for  range呈现了一种迭代化获取数据的方式:

在一个数据集合中,依次获取集合中的数据元素,而不用关注底层数据存储形式、以及数据扫描、获取方式!

rust 则是通过引入迭代器,将上述“迭代化获取数据”的方式作为一种通用的数据遍历方式!

迭代器允许我们迭代化访问一个连续的集合,例如数组、动态数组 VecHashMap 等,以及任何实现了迭代器特性的自定义数据集合;在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。

 

在rust中,遍历一个数组类似这样:

let  vec = vec![1, 2, 3, 4, 5];
for i in vec {
       println!("{}", i); 
}

看上去,这端代码和golang 的for range 几乎一致(只不过是关键字 换成了in);但实际上它们之间存在本质差别:golang 的for range 本质还是通过索引获取数据(所以可以拿到index),而rust 的for in是通过迭代器的迭代方法(next方法)获取数据!

为了理解这点,我们还得从rust 的 Iterator 特性说起......

Iterator (迭代器)

Iterator 是rust标准库中定义的一个trait,通常称之为“迭代器”。任何实现了该trait 的结构体,都可以用 for in 的方式进行迭代化数据访问。Iterator trait 中有两个关键的定义,其一是 type Item,表示该迭代器返回的数据类型;其二是 next方法,即迭代方法(通过循序调用next方法,遍历集合中的数据):

pub trait Iterator {
    /// The type of the elements being iterated over.
    type Item;
   /// Advances the iterator and returns the next value.
   /// Returns None when iteration is finished. Individual iterator implementations may choose to resume iteration, and so calling next() again may 
   /// or may not eventually start returning Some(Item) again at some point.
    fn next(&mut self) -> Option<Self::Item>;

  ... // 其他对集合操作的函数接口
}

 作为示例,我们可以写一个自己的简易迭代器:

struct Revolver {
    bullets: u16,
}

impl Revolver {
    fn new() -> Self {
        Revolver { bullets: 6 }
    }
    fn reload(&mut self) {
        self.bullets = 6;
    }
}

impl Iterator for Revolver {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.bullets > 0 {
            self.bullets -= 1;
            Some("Bang!".to_string())
        } else {
            None
        }
    }
}

#[test]
fn fire_all_bullets() {
     let  revolver = Revolver::new();
     for fired in revolver {
          println!("{}", fired);
     }
}

见代码片段,我们实现了一把左轮手枪,默认填6发子弹; 用for in 测试连续射击(fire_all_bullets),执行效果:

事实上,fire_all_bullets还有一个更直白的写法:

#[test]
fn fire_all_bullets() {
        let mut revolver = Revolver::new();
        loop {
            // 扣动扳机
            let fired = revolver.next();

            match fired {
                Some(bullet) => {
                    // 子弹发射
                    println!("{}", bullet)
                }
                // 没有子弹了,射击结束
                None => break,
            }
        }
}

上述代码和用for in 实现效果一样:Bang!Bang!Bang!Bang!Bang!Bang!

注意:当使用next() 手动遍历时,迭代器变量要设置为可变,这是因为迭代过程中会修改迭代器内部的数据!

事实上,loop展开的迭代器调用模式,才是迭代器真实执行的模型;for in 可以看做是编译器的语法糖,在编译期间将for in 迭代器展开为上述loop 的模型;只不过处理过程中,还会涉及一些数据克隆,引用转换,可变引用转换等,以方便使用;前一个测试中,for in 的revolver 没有加mut 也可以执行,这是因为for in 展开时,自己做了一次转换!

 

IntoIterator

第一章有个举例,for in 遍历数组:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr {
       println!("{}", i); 
}

按第二章的理解,for in 可以 直接作用于Vec,那么Vec应该实现了 Iterator?即Vec本身也是迭代器?

查看Vec实现并未发行Vec 实现了Iterator,相反它实现了IntoIterator 这个 trait。

功能如其名,IntoIterator 这个特性就是将实现了这个trait的数据转化为一个迭代器。

pub trait IntoIterator {
    /// The type of the elements being iterated over.
    type Item;

    /// Which kind of iterator are we turning this into?
    type IntoIter: Iterator<Item = Self::Item>;

    /// Creates an iterator from a value.
    fn into_iter(self) -> Self::IntoIter;
}

该trait主要包括三个成员: Item指定迭代器返回的元素;IntoIter 指定迭代器类型; into_iter实现由当前数据类型转化为指定迭代器的方法!

下面,我们用一个实例来演示一下IntoIterator 及Iterator 的关系和使用。

 

实战

考虑这样一个需求:对一个字符串,以空格为分割的方式,遍历获取每个单词。

直观的,要实现对一个字符串按空格进行逐个单词的迭代分割,我们需要引入一个位置标记(position),记录当前已经完成了单词分割的位置;当下一次请求单词时,从剩余的内容中找到下一个空格(或者直到内容末尾),将这部分内容作为分割的单词返回;同时,更新位置标记到当前返回的位置;最后,当位置标记到达原始字符串末尾时,处理完成,后续请求返回为None。

按照上面的思路,给出编码如下:

首先,自定义字符串数据结构:

// 定义MyString结构体
struct MyString {
    content: String,
}

通常,我们需要对现有数据进行额外处理时,为了不破坏原有的数据结构和实现,通过wapper包装是一个不错的选择。

其次,定义MyString的迭代器数据结构(根据上面的分析,需要一个位置标记来记录处理过程):

// 定义MyStringIterator结构体
struct MyStringIterator {
    content: String,
    position: usize,
}

 第三,为MyStringIterator 实现Iterator特性,使之成为迭代器:

impl Iterator for MyStringIterator {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let refx = self.content.as_str();
        let bytes = refx.as_bytes();
        while self.position < refx.len() && bytes[self.position] == b' ' {
            self.position += 1;
        }

        if self.position < refx.len() {
            let rest = &refx[self.position..];
            let idx = rest.find(' ').unwrap_or(rest.len());
            let result = rest[0..idx].to_string();
            self.position += idx;
            Some(result)
        } else {
            None
        }
    }
}

这里,我们指定返回迭代的元素为String,即每个单词以String类型返回;next实现中,会自动跳过连续的空格,通过 &str.find接口找到下一个空格的位置(如果找不到,则直到末尾),并进行字符切片截取,再转化为Some(String)返回。

第四,为MyString 实现IntoIterator 特性,使其可以转换为迭代器,方便对内容进行迭代处理:

impl IntoIterator for MyString {
    type Item = String;
    type IntoIter = MyStringIterator;

    fn into_iter(self) -> Self::IntoIter {
        MyStringIterator {
            content: self.content,
            position: 0,
        }
    }
}

需要注意的是,实现IntoIterator时,需要指定元素类型和迭代器类型,并且所指定的元素类型和迭代器要求的元素类型相匹配。

into_inter中,接受的self 是MyString的实例,并且获得其所有权;同时,将MyString中content变量绑定到 迭代器的content上;初始化位置为0.

第五, 测试应用

 #[test]
    fn test_my_string_iterator() {
        let my_string = MyString {
            content: "My name is LiLei".to_string(),
        };
        let mut iter = my_string.into_iter();
        assert_eq!("My", iter.next().unwrap());
        assert_eq!("name", iter.next().unwrap());
        assert_eq!("is", iter.next().unwrap());
        assert_eq!("LiLei", iter.next().unwrap());

        let nstr = MyString {
            content: "What's your name?".to_string(),
        };
        for word in nstr {
            println!("{}", word);
        }
    }

执行效果:

bingo, 测试通过。手动迭代assert,for 循环打印符合预期按空格分割的单词进行迭代输出。

 

再说for in Vec

前面说到了 Vec并没有实现Iterator,那么Vec在for in 结构里是怎么执行的呢?

前面也说到,for in 结构在rust 中是一种语法糖,它自己会进行类型推导:for in 结构后变量是迭代器时,对迭代器进行循环展开;当for in 结构后的变量非迭代器时,编译器会推断其必须可以进行迭代器转换,即它必须有一个 IntoIterator 实现,并尝试调用其into_iter方法获取迭代器(如果没有实现则会报错)。

所以,开始示例的for in 直接作用于数组:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr {
       println!("{}", i); 
}

等效于:

let  arr = vec![1, 2, 3, 4, 5];
for i in arr.into_iter {
       println!("{}", i); 
}

值得注意的是:

into_iter 进行迭代器转换时,会移走原始数据的所有权,导致原始数据不再可用!

如下测试代码:

#[test]
fn test_iter_vec() {
        let values = vec![1, 2, 3];
        for v in values {
            println!("{}", v)
        }
        println!("{:?}", values);
}

运行代码会报错:

错误提示说得明白,values 在再调用into_iter() 时被移交了所有权!

iter, iter_mut

在实际开发过程中,迭代器在数据处理上提供了很大的便利性;但是into_iter会拿走所有权会限制该模式在很多场景的使用。因此,很多数据类型在实现原始数据到迭代器的转化时,还提供了其他的转化方式。比如Vec还提供了 iter() 返回一个以不可变借用的方式遍历原数据的迭代器;iter_mut() 返回一个以可变借用的方式遍历原数据的迭代器。

以下是into_iter()iter()iter_mut()的一个对比测试, 大家可以实际跑一下,理解输出:

#[test]
fn test_iter_vec() {
        let values = vec![1, 2, 3];
        for v in values.into_iter() {
            println!("{}", v)
        }
        // 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
        // println!("{:?}",values);

        let values = vec![1, 2, 3];
        for v in values.iter() {
            println!("{}", v)
        }
        // 不会报错,因为 values_iter 只是借用了 values 中的元素
        println!("{:?}", values);

        let mut values = vec![1, 2, 3];
        for v in values.iter_mut() {
            *v = *v * 2;
        }
        // 输出[0, 4, 6]
        println!("{:?}", values);
}

注意:iter,iter_mut 不是 IntoIterator 的标准接口,用在for循环中时,必须显示调用!

当然,你也可以定义自己的转化函数,定义对原始数据的处理方式;但为了增加代码可读性,强烈建议遵循rust社区的一些通用编程实践!

最后

迭代器 Iterator中还有很多实用接口,比如map(),filter(), zip(),  count()等等。熟练掌握这些接口的功能和使用方法,能够让你在数据处理时更加游刃有余。这部分内容,就留给大家自行学习补充吧。

 

文章来自个人专栏
后台开发技术分享
18 文章 | 4 订阅
0条评论
0 / 1000
请输入你的评论
3
2