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
拥有query
和filename
的所有权,即使克隆会造成性能开销。
但在我们学过迭代器之后我们可以在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();
就是为了跳过这个值。 - 后面的
query
和filename
就依次使用next
方法来获取即可,这时候的query
和filename
就是拥有所有权的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
的安全问题了。