【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
拥有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
的安全问题了。
原文地址:https://blog.csdn.net/weixin_71793197/article/details/145251573
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!