优秀的编程知识分享平台

网站首页 > 技术文章 正文

Rust学习笔记(四十二)迭代器(下)改进命令行项目

nanyue 2024-09-01 20:35:58 技术文章 8 ℃

使用Iterator trait创建自定义迭代器

只需要实现Iterator trait的next方法就可以,例:

//src/lib.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    //此语法后面会学
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

#[test]
fn calling_next_directly() {//能从1迭代到5
    let mut counter = Counter::new();
    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

#[test]
fn using_other_iterator_trait_methods() {
    let sum: u32 = Counter::new()//新建一个1到5的迭代器
    .zip(Counter::new().skip(1))//生成一个跳过1的迭代器(2-5),然后与上面的迭代器生成一个元素是元组的新迭代器(1, 2)(2, 3)(3, 4) (4, 5)
    .map(|(a, b)| a * b)//元组两个元素相乘=2,6,12,20
    .filter(|x| x % 3 == 0)//留下能被3整除的是6,12
    .sum();//求和=18

    assert_eq!(sum, 18);
}

使用闭包和迭代器改进37节命令行项目

先找到Rust学习笔记(三十七)命令行项目实例里的代码,这里不给出完整代码

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        let query = args[1].clone();
        let filename = args[2].clone();
        Ok(Config { query, filename })
    }
}

这里由于new函数的参数是一个引用,所以需要clone出要查询的内容和文件名,然后构造Config的实例,这样Config实例才能拥有它俩的所有权。这里我们学习了迭代器,就可以传入一个拥有所有权的迭代器,就不用clone参数了。

//src/main.rs
use std::env;
use cmd_line::{self, Config};

use std::process;
fn main() {
    //unwrap_or_else用于Result后,Result是Ok时直接返回Ok内的值。
    //Err时执行参数中的闭包,类似于匿名函数,err是参数,{}里是函数体(后面详解)
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);//打印错误
        process::exit(1);//终止程序
    });
    //使用if let匹配错误情况,当run函数返回Err时会执行{}内逻辑
    if let Err(e) = cmd_line::run(config) {
      println!("Applocation error: {}", e);//打印错误
      process::exit(1);//终止程序
    };
}
//src/lib.rs
pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        args.next(); //由于第一个参数没有用,所以直接执行next消费掉
                     // let query = args[1].clone();
        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Did not get a query string!"),
        };
        // let filename = args[2].clone();
        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Did not get a file name!"),
        };
        Ok(Config { query, filename })
    }
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    // let mut results = Vec::new();
    // for line in contents.lines() {
    //     if line.contains(query) {
    //         results.push(line);
    //     }
    // }
    // results
    //减少临时变量
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

这里search改完了,search_case_insensitive也按照这个例子改。

性能比较

前面把for循环的search改成了使用迭代器实现,通过性能测试,发现迭代器版本性能更好。这是因为迭代器,作为一个高级的抽象,被编译成了与手写的底层代码性能差不多的代码。

零开销抽象(Zero-Cost Abstraction)

前面所说的高级的抽象就叫做零开销抽象或零成本抽象,使用它时不会引入额外的运行时开销。

作为另一个例子,这里有一些取自于音频解码器的代码。解码算法使用线性预测数学运算(linear prediction mathematical operation)来根据之前样本的线性函数预测将来的值。这些代码使用迭代器链来对作用域中的三个变量进行了某种数学计算:一个叫 buffer 的数据 slice、一个有 12 个元素的数组 coefficients、和一个代表位移位数的 qlp_shift。例子中声明了这些变量但并没有提供任何值;虽然这些代码在其上下文之外没有什么意义,不过仍是一个简明的现实中的例子,来展示 Rust 如何将高级概念转换为底层代码:

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}
```language

为了计算 prediction 的值,这些代码遍历了 coefficients 中的 12 个值,使用 zip 方法将系数与 buffer 的前 12 个值组合在一起。接着将每一对值相乘,再将所有结果相加,然后将总和右移 qlp_shift 位。

像音频解码器这样的程序通常最看重计算的性能。这里,我们创建了一个迭代器,使用了两个适配器,接着消费了其值。Rust 代码将会被编译为什么样的汇编代码呢?好吧,在编写本书的这个时候,它被编译成与手写的相同的汇编代码。遍历 coefficients 的值完全用不到循环:Rust 知道这里会迭代 12 次,所以它“展开”(unroll)了循环。展开是一种移除循环控制代码的开销并替换为每个迭代中的重复代码的优化。

所有的系数都被储存在了寄存器中,这意味着访问他们非常快。这里也没有运行时数组访问边界检查。所有这些 Rust 能够提供的优化使得结果代码极为高效。现在知道这些了,请放心大胆的使用迭代器和闭包吧!他们使得代码看起来更高级,但并不为此引入运行时性能损失。

最近发表
标签列表