💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:Y小夜-CSDN博客
目录
🎯使用迭代器并去掉clone
🎯直接使用返回的迭代器
🎯使用Iterator trait代替代替索引
🎯使用迭代器适配器来使代码更简明
🎯选择循环或者迭代器
🎯性能对比:循环vs迭代器
🎯使用迭代器并去掉clone
我们增加了一些代码获取一个
String
slice 并创建一个Config
结构体的实例,它们索引 slice 中的值并克隆这些值以便Config
结构体可以拥有这些值。impl Config {pub fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();let ignore_case = env::var("IGNORE_CASE").is_ok();Ok(Config {query,file_path,ignore_case,})} }
那时我们说过不必担心低效的
clone
调用了,因为将来可以对它们进行改进。好吧,就是现在!起初这里需要
clone
的原因是参数args
中有一个String
元素的 slice,而build
函数并不拥有args
。为了能够返回Config
实例的所有权,我们需要克隆Config
中字段query
和file_path
的值,这样Config
实例就能拥有这些值。
🎯直接使用返回的迭代器
打开 I/O 项目的 src/main.rs 文件,它看起来应该像这样:
fn main() {let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {eprintln!("Problem parsing arguments: {err}");process::exit(1);});// --snip-- }
fn main() {let config = Config::build(env::args()).unwrap_or_else(|err| {eprintln!("Problem parsing arguments: {err}");process::exit(1);});// --snip-- }
env::args
函数返回一个迭代器!不同于将迭代器的值收集到一个 vector 中接着传递一个 slice 给Config::build
,现在我们直接将env::args
返回的迭代器的所有权传递给Config::build
。不能编译因为我们还需更新函数体:
impl Config {pub fn build(mut args: impl Iterator<Item = String>,) -> Result<Config, &'static str> {// --snip--
env::args
函数的标准库文档显示,它返回的迭代器的类型为std::env::Args
,同时这个类型实现了Iterator
trait 并返回String
值。因为我们拥有
args
的所有权,并且将通过对其进行迭代来改变args
,所以我们可以将mut
关键字添加到args
参数的规范中以使其可变。
🎯使用Iterator trait代替代替索引
接下来,我们将修改
Config::build
的内容。因为args
实现了Iterator
trait,因此我们知道可以对其调用next
方法!impl Config {pub fn build(mut args: impl Iterator<Item = String>,) -> Result<Config, &'static str> {args.next();let query = match args.next() {Some(arg) => arg,None => return Err("Didn't get a query string"),};let file_path = match args.next() {Some(arg) => arg,None => return Err("Didn't get a file path"),};let ignore_case = env::var("IGNORE_CASE").is_ok();Ok(Config {query,file_path,ignore_case,})} }
请记住
env::args
返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用next
并不对返回值做任何操作。之后对希望放入Config
中字段query
调用next
。如果next
返回Some
,使用match
来提取其值。如果它返回None
,则意味着没有提供足够的参数并通过Err
值提早返回。对file_path
值进行同样的操作。
🎯使用迭代器适配器来使代码更简明
I/O 项目中其他可以利用迭代器的地方是
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 }
可以通过使用迭代器适配器方法来编写更简明的代码。这也避免了一个可变的中间
results
vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理results
vector 的并发访问。pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {contents.lines().filter(|line| line.contains(query)).collect() }
回忆
search
函数的目的是返回所有contents
中包含query
的行。类似于示例 13-16 中的filter
例子,可以使用filter
适配器只保留line.contains(query)
返回true
的那些行。接着使用collect
将匹配行收集到另一个 vector 中。这样就容易多了!尝试对search_case_insensitive
函数做出同样的使用迭代器方法的修改吧。
🎯选择循环或者迭代器
接下来的逻辑问题就是在代码中应该选择哪种风格:大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector,(迭代器)代码则更关注循环的目的。这抽象掉那些老生常谈的代码,这样就更容易看清代码所特有的概念,比如迭代器中每个元素必须面对的过滤条件。
🎯性能对比:循环vs迭代器
为了决定使用哪个实现,我们需要知道哪个版本的
search
函数更快一些:是直接使用for
循环的版本还是使用迭代器的版本。我们运行了一个性能测试,通过将阿瑟·柯南·道尔的“福尔摩斯探案集”的全部内容加载进
String
并寻找其中的单词 “the”。如下是for
循环版本和迭代器版本的search
函数的性能测试结果:test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
结果迭代器版本还要稍微快一点!这里我们将不会查看性能测试的代码,我们的目的并不是为了证明它们是完全等同的,而是得出一个怎样比较这两种实现方式性能的基本思路。
作为另一个例子,这里有一些取自于音频解码器的代码。解码算法使用线性预测数学运算(linear prediction mathematical operation)来根据之前样本的线性函数预测将来的值。这些代码使用迭代器链对作用域中的三个变量进行某种数学计算:一个叫
buffer
的数据 slice、一个有 12 个元素的数组coefficients
、和一个代表位移位数的qlp_shift
。例子中声明了这些变量但并没有提供任何值;虽然这些代码在其上下文之外没有什么意义,不过仍是一个简明的现实中的例子,来展示 Rust 如何将高级概念转换为底层代码: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; }
为了计算
prediction
的值,这些代码遍历了coefficients
中的 12 个值,使用zip
方法将系数与buffer
的前 12 个值组合在一起。接着将每一对值相乘,再将所有结果相加,然后将总和右移qlp_shift
位。像音频解码器这样的程序通常最看重计算的性能。这里,我们创建了一个迭代器,使用了两个适配器,接着消费了其值。那么这段 Rust 代码将会被编译为什么样的汇编代码呢?好吧,在编写本书的这个时候,它被编译成与手写的相同的汇编代码。遍历
coefficients
的值完全用不到循环:Rust 知道这里会迭代 12 次,所以它“展开”(unroll)了循环。展开是一种将循环迭代转换为重复代码,并移除循环控制代码开销的代码优化技术。