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