前言#
昨天我们学了Iterator迭代器
今天我们来用所学的知识优化下之前写的那个minigrep
优化前的代码#
lib.rs
use std::{error::Error, fs, env};
#[derive(Debug)]
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
pub ignore: bool,
}
pub enum ConfigField {
Query,
FilePath,
}
impl<'a> Config<'a> {
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
let ignore = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore })
}
pub fn get(&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => self.query,
ConfigField::FilePath => self.file_path,
}
}
}
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
dbg!(&config);
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
let mut res = vec![];
for line in poem.lines() {
if config.ignore {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
} else {
if line.contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
}
}
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{process};
#[test]
fn search() {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: true
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
#[test]
fn sensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: false
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("");
assert_eq!(res, target)
}
#[test]
fn insensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: true
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
}
main.rs
use std::{env, process};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
dbg!(res);
} 优化#
还记得我们怎么把args里的参数放到Config实例中的吗?官方推荐的是clone复制一份数据出来.而我这里用的则是生命周期.
其中的区别是可维护性和性能 。
我当时也是推荐的clone,不过还是用生命周期来写,毕竟是在学习阶段.
扯远了,回到代码中.
优化点1: 直接获取迭代器中的数据#
env::args()返回的是一个迭代器,那么我们完全可以从迭代器里获取数据.
来改下我们的lib.rs中代码
pub struct Config {
query: String,
file_path: String,
pub ignore: bool,
}
pub enum ConfigField {
Query,
FilePath,
}
impl Config {
pub fn get(&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => &self.query,
ConfigField::FilePath => &self.file_path,
}
}
pub fn new<T: Iterator<Item = String>>(mut args: T) -> Result<Config, &'static str> {
// 第一个是执行的exe文件路径
args.next();
let query = match args.next() {
Some(v) => v,
None => return Err("get first field 'query' error"),
};
let file_path = match args.next() {
Some(v) => v,
None => return Err("get first field 'query' error"),
};
let ignore = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore,
})
}
}然后再来改下main函数
fn main() {
let args = env::args();
let config = Config::new(args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
dbg!(res);
} 然后终端重新输入指令

可以看到使用迭代器我们省去了生命周期或者clone方法.
这样既好维护又不会失去性能.
优化点2: 直接使用filter#
我们是通过&str.contains方法来判断是否存在匹配项的,其实lines返回的也是一个迭代器,我们完全可以使用filter顺便过滤了.
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
dbg!(&config);
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
Ok(if config.ignore {
insensitive(poem, config)
} else {
sensitive(poem, config)
})
}
fn sensitive (poem: String, config: Config) -> Vec<String> {
poem.lines()
.filter(|line| line.contains(config.get(ConfigField::Query)))
.map(|item| item.to_string())
.collect()
}
fn insensitive (poem: String, config: Config) -> Vec<String> {
poem.lines()
.filter(|line| line.to_lowercase().contains(config.get(ConfigField::Query)))
.map(|item| item.to_string())
.collect()
}额,有点长了,因为我这里还需要把这个匹配到的数据传出去,所以多了一步map转换类型以及获取所有权.
循环(loop)还是迭代(iterate)#
在学迭代器之前,我们遍历都是通过for/while/loop的方式,而学完迭代器之后我们又多了种遍历的方式.
那么loop和iterate之间哪个更快呢?
为了回答这个问题,rust官方做了一个比较,在一本书*The Adventures of Sherlock Holmes*[3]里找一个词,用找到这个词(所有,并不是找到一个就停止)所用的时间来判断.
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)可以明显看到迭代器所用的时间少很多.
迭代器是rust中少有的*zero-cost abstractions,*也就是零成本抽象概念,换句话说就是不会对runtime有任何负担,在编译阶段就被转换成类似我们手动重复写的低级代码了
再来看个例子
let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
这是一段音频解码器的解码算法,基于线性预测来计算,根据之前收集到的数据来线性预测将来的值.
这里有三个变量
coefficients,它是一个数组, 有12个元素buffer,他也是一个数组,个数未知.qlp_shift,是一个i16类型的整数.
三者做的运算
coefficients和buffer中第i-12..i个进行zip,什么是zip呢?就像拉链一样
来看下zip的例子,它返回一个新的迭代器,所以它是一个迭代器适配器.
fn main() {
let v1 = vec![1, 2, 3, 4];
let v2 = vec![5, 6, 7, 8];
let a = v1.iter();
let b = v2.iter();
let c: Vec<_> = a.zip(b).collect();
}然后我们来看下c的数据

可以看到它返回的迭代器item是一个元组包裹着两个迭代器里的item.
然后将元组里的元素相乘,之后再把迭代器里的数据相加.
sum方法前面说过了,就是将迭代器里的元素相加返回一个结果.
最后再向右偏移qlp_shift个位置.
额,好像和我们的题目没啥关系?
其实还没说到点上,对于这样的代码,rust在编译阶段不会去遍历,而是重复12次,因为这里只有12个元素,就像是我们去手写一样.
rust开发团队称之为unrolls.也就是展开.
通过两个小例子,rust团队是想说: 尽情的用迭代器吧,不用害怕.
总结#
今天我们优化了下之前写的minigrep小工具,然后又看了下迭代器和循环之间的区别,了解迭代器的部分底层相关知识.
参考#
- ^rust-13.3-improving-our-I/O-project https://doc.rust-lang.org/book/ch13-03-improving-our-io-project.html#improving-our-io-project
- ^rust-13.4-comparing-performance: Loops vs Iterators https://doc.rust-lang.org/book/ch13-04-performance.html#comparing-performance-loops-vs-iterators
- ^the adventures of sherlock holmes https://www.bing.com/ck/a?!&&p=dd651b5613da6ee2JmltdHM9MTY3MjA5OTIwMCZpZ3VpZD0xMmFlZWUwMi0wMDgyLTZiYTgtM2ZmNy1mZTY2MDQ4MjY5ZWQmaW5zaWQ9NTI3NQ&ptn=3&hsh=3&fclid=12aeee02-0082-6ba8-3ff7-fe66048269ed&psq=the+adventures+of+sherlock+holmes&u=a1aHR0cHM6Ly9wb2RjYXN0cy5hcHBsZS5jb20vdXMvcG9kY2FzdC90aGUtYWR2ZW50dXJlcy1vZi1zaGVybG9jay1ob2xtZXMtYnktc2lyLWFydGh1ci1jb25hbi1kb3lsZS9pZDcyNzQwNTE0OQ&ntb=1
发布于 2022-12-28 15:21・IP 属地广东
