13.9 使用闭包和迭代器改进I/O项目13.9.0. 写在正文之前Rust语言在设计过程中受到了很多语言的启发而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。在本章中我们会讨论 Rust 的一些特性这些特性与许多语言中通常称为函数式的特性相似- 闭包- 迭代器-使用闭包和迭代器改进I/O项目本文- 闭包和迭代器的性能13.9.1. 回顾本篇文章会以第12章中的grep项目为例演示使用闭包和迭代器改进I/O项目在此之前我们先回顾一下。第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print)是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。这个项目分为这么几步- 接收命令行参数- 读取文件- 重构改进模块和错误处理- 使用TDD测试驱动开发开发库功能- 使用环境变量- 将错误信息写入标准错误而不是标准输出lib.rs: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]) - ResultConfig, 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(), Boxdyn 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 searcha(query: str, contents: a str) - Veca str { let mut results Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search_case_insensitivea(query: str, contents: a str) - Veca str { let mut results Vec::new(); let query query.to_lowercase(); for line in contents.lines() { if line.to_lowercase().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:VecString 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]) - ResultConfig, 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拥有query和filename的所有权即使克隆会造成性能开销。但在我们学过迭代器之后我们可以直接把迭代器传给new从而让它获得所有权。我们还可以通过迭代器实现长度检查和索引使new函数的责任范围更加明确。改new函数之前我们得先改main函数对输入参数的处理方法原本是let args:VecString 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它实现了Iteratortrait所以是一个迭代器。现在来修改new函数impl Config { pub fn new(mut args: std::env::Args) - ResultConfig, 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();就是为了跳过这个值。后面的query和filename就依次使用next方法来获取即可这时候的query和filename就是拥有所有权的String。由于next的返回值是Option所以可以使用unwrap来解包。13.9.3.search函数的改进目前的search函数是这样的pub fn searcha(query: str, contents: a str) - Veca 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 searcha(query: str, contents: a str) - Veca str { contents.lines().filter(|line| line.contains(query)).collect() }通过在闭包中使用contains来检查是否包含关键字就实现了同样的逻辑。既然普通的搜索函数能使用迭代器同样的大小写不敏感的搜索函数也可以使用迭代器pub fn search_case_insensitivea(query: str, contents: a str) - Veca str { let query query.to_lowercase(); contents .lines() .filter(|line| line.to_lowercase().contains(query)) .collect() }这里仍然先把查询字符串转成小写。对每一行line.to_lowercase()会生成一个仅用于contains检查的临时小写String真正被收集进结果的仍是原始的line指向contents的str。这样才能让返回类型Veca str合法。如果写成contents.to_lowercase().lines()...collect()迭代器产出的引用会指向临时的小写String这些引用不能作为指向原始contents的a str返回——代码将无法通过编译。不管从代码量还是可读性上比使用filter的方法都更好。此外filter方法还减少了临时变量。消除可变状态(let mut results Vec::new();)使我们可以在未来通过并行化来提升搜索效率因为无需考虑并发访问results的安全问题了。
【Rust自学】13.9. 使用闭包和迭代器改进IO项目
13.9 使用闭包和迭代器改进I/O项目13.9.0. 写在正文之前Rust语言在设计过程中受到了很多语言的启发而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。在本章中我们会讨论 Rust 的一些特性这些特性与许多语言中通常称为函数式的特性相似- 闭包- 迭代器-使用闭包和迭代器改进I/O项目本文- 闭包和迭代器的性能13.9.1. 回顾本篇文章会以第12章中的grep项目为例演示使用闭包和迭代器改进I/O项目在此之前我们先回顾一下。第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print)是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。这个项目分为这么几步- 接收命令行参数- 读取文件- 重构改进模块和错误处理- 使用TDD测试驱动开发开发库功能- 使用环境变量- 将错误信息写入标准错误而不是标准输出lib.rs: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]) - ResultConfig, 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(), Boxdyn 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 searcha(query: str, contents: a str) - Veca str { let mut results Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search_case_insensitivea(query: str, contents: a str) - Veca str { let mut results Vec::new(); let query query.to_lowercase(); for line in contents.lines() { if line.to_lowercase().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:VecString 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]) - ResultConfig, 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拥有query和filename的所有权即使克隆会造成性能开销。但在我们学过迭代器之后我们可以直接把迭代器传给new从而让它获得所有权。我们还可以通过迭代器实现长度检查和索引使new函数的责任范围更加明确。改new函数之前我们得先改main函数对输入参数的处理方法原本是let args:VecString 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它实现了Iteratortrait所以是一个迭代器。现在来修改new函数impl Config { pub fn new(mut args: std::env::Args) - ResultConfig, 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();就是为了跳过这个值。后面的query和filename就依次使用next方法来获取即可这时候的query和filename就是拥有所有权的String。由于next的返回值是Option所以可以使用unwrap来解包。13.9.3.search函数的改进目前的search函数是这样的pub fn searcha(query: str, contents: a str) - Veca 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 searcha(query: str, contents: a str) - Veca str { contents.lines().filter(|line| line.contains(query)).collect() }通过在闭包中使用contains来检查是否包含关键字就实现了同样的逻辑。既然普通的搜索函数能使用迭代器同样的大小写不敏感的搜索函数也可以使用迭代器pub fn search_case_insensitivea(query: str, contents: a str) - Veca str { let query query.to_lowercase(); contents .lines() .filter(|line| line.to_lowercase().contains(query)) .collect() }这里仍然先把查询字符串转成小写。对每一行line.to_lowercase()会生成一个仅用于contains检查的临时小写String真正被收集进结果的仍是原始的line指向contents的str。这样才能让返回类型Veca str合法。如果写成contents.to_lowercase().lines()...collect()迭代器产出的引用会指向临时的小写String这些引用不能作为指向原始contents的a str返回——代码将无法通过编译。不管从代码量还是可读性上比使用filter的方法都更好。此外filter方法还减少了临时变量。消除可变状态(let mut results Vec::new();)使我们可以在未来通过并行化来提升搜索效率因为无需考虑并发访问results的安全问题了。