自学内容网 自学内容网

【Rust自学】13.9. 使用闭包和迭代器改进IO项目

13.9.0. 写在正文之前

Rust语言在设计过程中收到了很多语言的启发,而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。

在本章中,我们会讨论 Rust 的一些特性,这些特性与许多语言中通常称为函数式的特性相似:

  • 闭包
  • 迭代器
  • 使用闭包和迭代器改进I/O项目(本文)
  • 闭包和迭代器的性能

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

13.9.1. 回顾

本篇文章会以第12章中的grep项目为例演示使用闭包和迭代器改进I/O项目,在此之前我们先回顾一下。

第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:

  • 接收命令行参数
  • 读取文件
  • 重构:改进模块和错误处理
  • 使用TDD(测试驱动开发)开发库功能
  • 使用环境变量
  • 将错误信息写入标准错误而不是标准输出

lib.rs:

```rust
use std::error::Error;  
use std::fs;  
  
pub struct Config {  
    pub query: String,  
    pub filename: String,  
    pub case_sensitive: bool,  
}  
  
impl Config {  
    pub fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        let case_sensitive = std::env::var("CASE_INSENSITIVE").is_err();  
        Ok(Config { query, filename, case_sensitive})  
    }  
}  
  
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {  
    let contents = fs::read_to_string(config.filename)?;  
    let results = if config.case_sensitive {  
        search(&config.query, &contents)  
    } else {  
        search_case_insensitive(&config.query, &contents)  
    };  
    for line in results {  
        println!("{}", line);  
    }  
    Ok(())  
}  
  
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  
}  
  
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {  
    let mut results = Vec::new();  
    let query = query.to_lowercase();  
    for line in contents.to_lowercase().lines() {  
        if line.contains(&query) {  
            results.push(line);  
        }  
    }  
    results  
}  
  
#[cfg(test)]  
mod tests {  
    use super::*;  
  
    #[test]  
    fn case_sensitive() {  
        let query = "duct";  
        let contents = "\  
Rust:  
safe, fast, productive.  
Pick three.  
Duct tape.";  
  
        assert_eq!(vec!["safe, fast, productive."], search(query, contents));  
    }  
  
    #[test]  
    fn case_insensitive() {  
        let query = "rUsT";  
        let contents = "\  
Rust:  
safe, fast, productive.  
Pick three.  
Trust me.";  
  
        assert_eq!(  
            vec!["Rust:", "Trust me."],  
            search_case_insensitive(query, contents)  
        );  
    }  
}

main.rs:

use std::env;  
use std::process;  
use minigrep::Config;  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args).unwrap_or_else(|err| {  
        eprintln!("Problem parsing arguments: {}", err);  
        process::exit(1);  
    });  
    if let Err(e) = minigrep::run(config) {  
        eprintln!("Application error: {}", e);  
        process::exit(1);  
    }  
}

13.9.2. new函数的改进

看一下lib.rs里的new函数:

impl Config {  
    pub fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        let case_sensitive = std::env::var("CASE_INSENSITIVE").is_err();  
        Ok(Config { query, filename, case_sensitive})  
    }  
}

其中的这两行:

let query = args[1].clone();  
let filename = args[2].clone();

使用了克隆的方法。这是因为传进去的参数是&[String],没有所有权,但是Config结构体要求持有所有权。只有使用克隆才能让Config拥有queryfilename的所有权,即使克隆会造成性能开销。

但在我们学过迭代器之后我们可以在new函数里直接使用迭代器作为它的参数从而获得所有权。我们还可以通过迭代器实现长度检查和索引,使new函数的责任范围更加明确。

new函数之前我们得先改main函数对输入参数的处理方法,原本是:

let args:Vec<String> = env::args().collect();  
let config = Config::new(&args).unwrap_or_else(|err| {  
    eprintln!("Problem parsing arguments: {}", err);  
    process::exit(1);  
});  

现在我们去掉collect方法,直接把env::args()所获得的参数传给new函数:

let config = Config::new(env::args()).unwrap_or_else(|err| {  
    eprintln!("Problem parsing arguments: {}", err);  
    process::exit(1);  
});  

env::args()的返回类型是std::env::Args,它实现了Iterator trait,所以是一个迭代器。

现在来修改new函数:

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();  
        let query = args.next().unwrap();  
        let filename = args.next().unwrap();  
        let case_sensitive = std::env::var("CASE_INSENSITIVE").is_err();  
        Ok(Config { query, filename, case_sensitive})  
    }  
}
  • 把形参args的类型改为std::env::Args,还得声明为可变变量加上mut,因为next方法是消耗性迭代器
  • 函数体里有一行只写了args.next();是因为env::args()获取的第一个值是程序的名称而不是参数,写args.next();就是为了跳过这个值。
  • 后面的queryfilename就依次使用next方法来获取即可,这时候的queryfilename就是拥有所有权的String。但是next的返回值是Option枚举,所以可以使用unwrap来解包。

13.9.3. Search函数的改进

目前的Search函数是这样的:

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()返回的也是迭代器,我们在这里手动地判断是否包含关键字,也就是query所存储的字符串,如果包含就把这行放到Vector里,最后把Vector返回。

对于在迭代器中寻找符合某个要求的目标元素组成新的迭代器,可以使用filter方法:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {  
    contents.lines().filter(|line| line.contains(query)).collect()  
}

通过在闭包中使用contains来检查是否包含关键字就实现了同样的逻辑。

既然普通的搜索函数能使用迭代器,同样的,大小写不敏感的搜索函数也可以使用迭代器:

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {  
    contents.to_lowercase()  
        .lines()  
        .filter(|line| line.contains(&query.to_lowercase()))  
        .collect()  
}

注意,query.to_lowercase()得加&,因为query.to_lowercase()会生成String类型,而contains方法接收&str,所以不能直接传query.to_lowercase(),只有传引用进去,也就是&query.to_lowercase()才能正确执行。

转换为&str不仅可以加&,当然也可以用as_str方法:

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {  
    contents.to_lowercase()  
        .lines()  
        .filter(|line| line.contains(query.to_lowercase().as_str()))  
        .collect()  
}

不管从代码量还是可读性上比,使用filter的方法都更好。此外filter方法还减少了临时变量。消除可变状态(let mut results = Vec::new();)使我们可以在未来通过并行化来提升搜索效率,因为无需考虑并发访问results的安全问题了。


原文地址:https://blog.csdn.net/weixin_71793197/article/details/145251573

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!