写给前端看的Rust教程(17)迭代

3,570 阅读6分钟

原文:24 days from node.js to Rust

前言

JavaScript的数组和循环转换到Rust的对应功能,需要了解一点新的概念和数据类型。在某些方面RustJavaScript的写法要简洁,而其它地方则是要比多出十倍不止

我们在 教程7 中已经接触过VecVecDeque ,紧随而来的一个难点就是迭代,很多在JavaScript中的数组方法在Rust中也存在,不过十倍一个Iterator数据结构所封装

正文

vec![], Vec、VecDeque

Rust中的数组必须在初始化的时候所有的成员都必须初始化,长度也是固定的。你可以修改每个成员的数值,但不能改变数组的长度,例如下面的代码是不许可的:

let mut numbers = [1, 2, 3, 4, 5];
numbers.push(7); // no method named `push` found for array `[{integer}; 5]`
println!("{:?}", numbers);

如果想要动态的改变长度,需要用Vec 和 VecDequeVec可以在尾部增减内容,VecDeque在头部和尾部都可以增减内容。利用vec![]宏我们可以轻松的创建Vec

let mut numbers = vec![1, 2, 3, 4, 5];  // ⬅ 注意 vec! 宏
numbers.push(7);
println!("{:?}", numbers);

循环

for ( … ; … ; … )

Rust不支持for ( … ; … ; … )形式的循环,Rust采用的是for…in表达式与 范围运算符结合来实现循环

JavaScript版:

let max = 4;
for (let i = 0; i < max; i++) {
  console.log(i);
}

Rust版:

let max = 4;
for i in 0..max {
  println!("{}", i);
}

结果:

1
2
3

for…in

Rust中没有JavaScriptObjectfor…in不能用来遍历对象的key值,Rust中有HashMap(详见 教程8 ),如果想遍历的话可以使用.keys()方法

注意:.keys()方法遍历key值的顺序是不固定的,不要依赖顺序

TypeScript版:

let obj: any = {
  key1: "value1",
  key2: "value2",
};
for (let prop in obj) {
  console.log(`${prop}: ${obj[prop]}`);
}

Rust版:

let obj = HashMap::from([
  ("key1", "value1"),
  ("key2", "value2")
]);
for prop in obj.keys() {
  println!("{}: {}", prop, obj.get(prop).unwrap());
}

结果:

key1: value1
key2: value2

for…of

JavaScript中的for…ofRust中的for…in差不多就是一一对应的关系

TypeScript版:

let numbers = [1, 2, 3, 4, 5];
for (let number of numbers) {
  console.log(number);
}

Rust版:

let numbers = [1, 2, 3, 4, 5];
for number in numbers {
  println!("{}", number);
}

结果:

1
2
3
4
5

while (!done)

JavaScript中我们使用while (!done),在Rust中对应的是while let。下面这段代码只要.doWork()返回了Some(),则循环就会继续(你可以用ResultOk或其它类型来做尝试)

struct Worker {
  data: Vec<&'static str>,
}
impl Worker {
  fn doWork(&mut self) -> Option<&'static str> {
    self.data.pop()
  }
}
let mut obj = Worker {
  data: vec!["a", "b", "c"],
};

while let Some(data) = obj.doWork() {
  println!("{}", data);
}

结果:

c
b
a

do … while

Rust没有do … while,你可以用loop表达式替代

while (true) …

Rust中采用loop表达式,看起来更加直观简单:

let mut n = 0;
loop {
  n += 1;
  if n > 3 {
    break;
  }
}
println!("Finished. n={}", n);

结果:

Finished. n=4

Labels、break、continue

RustLabels的工作方式和JavaScript中的类似,区别只是Rust中的要添加一个顿号前缀

TypeScript版:

outer: while (true) {
  while (true) {
    break outer;
  }
}

Rust版:

'outer: loop {
  loop {
    break 'outer;
  }
}

break & loop

loop表达式可以有返回值,这比在循环外设置初始值,在循环内更新要好得多

let value = loop {
  if true {
    break "A";
  } else {
    break "B";
  }
};
println!("Loop value is: {}", value);

结果:

Loop value is: A

迭代器

Rust采用迭代器来处理序列,迭代器可以依托链式来生成更多的迭代器。不同于JavaScript中的迭代器方法,Rust迭代器是lazy的,它们只有在调用的时候才执行

所有的迭代器都实现了Iterator trait,这使得每个迭代器的接口都很相似,这个trait和其它一些基础性trait不太一样,它有一个名为Item的关联类型作为遍历对象的占位符

trait中的关联类型和泛型类似,它们是一种类型的占位符,更多信息可见 The Rust Book, ch 19.03: Advanced Traits

用法

Vec并不是迭代器,我们需要调用一个方法来创建迭代器,又因为迭代器是lazy的,我们必须要调用方法才能得到数值,所以我们都要调用两次方法:

let list = vec![1, 2, 3];
let doubled: Vec<_> = list
  .iter()
  .map(|num| num * 2)
  .collect();
println!("{:?}", doubled);

很多数据结构中.iter()都能返回一个迭代器,迭代器的.collect()方法将会对其进行消费

可以直接调用.next()方法获取迭代器值

当自己你使用.collect()时可能会遇到一个错误:error[E0282]: type annotations needed

let list = vec![1, 2, 3];
let doubled = list.iter().map(|num| num * 2).collect();

报错:

error[E0282]: type annotations needed
  --> crates/day-17/iterators/src/main.rs:13:7
   |
13 |   let doubled = list.iter().map(|num| num * > 2).collect();
   |       ^^^^^^^ consider giving `doubled` a type

For more information about this error, try `rustc --> explain E0282`.

一开始你可能不知道为什么需要注明类型,Rust知道map中数据的类型,也知道返回的类型,那为什么我们还需要像下面这样注明类型呢?

let list = vec![1, 2, 3];
let doubled: Vec\<i32>= list.iter().map(|num| num * 2).collect();

Rust确实是知晓元素的类型i32,但不知道Vec<>类型。当Rust知晓其具体类型时,你可以将类型用_省略,例如Vec<_>

.iter()返回的是元素的不可变引用,如果你希望改变元素,则用.iter_mut()代替

.filter()

迭代器的.filter()方法会返回一个新的迭代器:

TypeScript版:

let numbers = [1, 2, 3, 4, 5];
let even = numbers.filter((x) => x % 2 === 0);
console.log(even);

Rust版:

t numbers = [1, 2, 3, 4, 5];
let even: Vec<_> = numbers.iter().filter(|x| *x % 2 == 0).collect();
println!("{:?}", even);

结果:

[2, 4]

注意上述代码x变量前面的*,这是因为.filter()引用了一层,大多数迭代器也会引用一层,如果我们希望得到引用,就需要解引用

.find()

.find(predicate)本质上就是一个.filter(predicate).next(),它会不断消费迭代直到满足判断条件返回了指定值

JavaScript版:

et numbers = [1, 2, 3, 4, 5];
let firstEven = numbers.find((x) => x % 2 === 0);
console.log(firstEven);

Rust版:

let numbers = [1, 2, 3, 4, 5];
let first_even = numbers.iter().find(|x| *x % 2 == 0);
println!("{:?}", first_even.unwrap());

结果:

2

.find()可以多次调用,具有记忆性,但在JavaScript中是不能这么干的

let numbers = [1, 2, 3, 4, 5];
let mut iter = numbers.iter(); // Note, our iter is mut
let first_even = iter.find(|x| *x % 2 == 0);
println!("{:?}", first_even.unwrap());
let second_even = iter.find(|x| *x % 2 == 0);
println!("{:?}", second_even.unwrap());
2
4

.forEach()

.for_each()会立即消费迭代器,你可以在迭代器链的末端使用它来操作每个元素(使用普通循环通常是一种更具可读性的选择)

JavaScript版:

et numbers = [1, 2, 3];
numbers.forEach((x) => console.log(x));

Rust版:

let numbers = [1, 2, 3];
numbers.iter().for_each(|x| println!("{}", x));

结果:

1
2
3

.join()

可以在数组和Vec上使用.join(),切不需要迭代器

JavaScript版:

let names = ["Sam", "Janet", "Hunter"];
let csv = names.join(", ");
console.log(csv);

Rust版:

let names = ["Sam", "Janet", "Hunter"];
let csv = names.join(", ");
println!("{}", csv);

结果:

Sam, Janet, Hunter

.map()

.map()会返回一个新的迭代器

JavaScript版:

let list = [1, 2, 3];
let doubled = list.map((x) => x * 2);
console.log(doubled);

Rust版:

let list = vec![1, 2, 3];
let doubled: Vec<_> = list.iter().map(|num| num * 2).collect();
println!("{:?}", doubled)

结果:

[2, 4, 6]

如何返回一个迭代器

当可以返回迭代器本身时,使用.collect()返回特定的数据结构是一种糟糕的形式。返回迭代器可以保持灵活性,并保留Rust程序员所期望的惰性求值。由于基本的Iterator是一个trait,我们可以像在之前的指南中返回闭包和其他值一样返回它

下面这个例子返回了一个Iterator而不是Vec<&String>

struct Names {
  names: Vec<String>,
}

impl Names {
  fn search<T: AsRef<str>>(&self, re: T) -> impl Iterator<Item = &String> {
    let regex = regex::Regex::new(re.as_ref()).unwrap();
    self.names.iter().filter(move |name| regex.is_match(name))
  }
}

如果对AsRef<str>感到困惑,请重新阅读 教程12

相关阅读

总结

将迭代器和列表如何工作的心智模型进行恰如其分的转换是十分重要的,如果你熟悉JavaScript中的函数式编程风格,那对Rust的学习是很有好处的,尽管Rust不是一个纯函数式编程语言