Let's Go Rust 系列之for对比

前言
在golang中提供了for range 语法糖帮助我们方便的遍历数据或切片,也可以遍历map和channel;在rust中则提供了 for in 语法糖帮助我们方便的遍历Array,Vector或切片,也可以遍历map。本文将会通过多种类型的例子介绍两种语言中这些语法糖背后的机制以及使用不当可能遇到的陷阱。
遍历方式对比
Golang
我们先来看Golang中的三种遍历方式:

arr := []int{2, 4, 6}// 只要索引 for i := range arr { fmt.Println(i) }// 索引和数值 for i, v := range arr { fmt.Println(i, v) }// 只要数值 for _, v := range arr { fmt.Println(v) }

输出
0
1
2
0 2
1 4
2 6
2
4
6
Rust
首先我们要了解在rust中遍历arr有下面四种不同的方法,本质都是将当前arr转为了相应的iterator。
let arr = vec![1, 2, 3]; for a in arr { println!("{}", a); }let arr = vec![1, 2, 3]; for a in arr.into_iter() { println!("{}", a); }let arr = vec![1, 2, 3]; for a in arr.iter() { println!("{}", a); }let mut arr = vec![1, 2, 3]; for a in arr.iter_mut() { println!("{}", a); }

其中 for a in arr 等价于for a in arr.into_iter() ,这种遍历方式会把当前arr中的变量 move 掉,执行遍历后,arr无法再使用。
for a in arr.iter() 返回arr中每一项的不可变借用,for a in arr.iter_mut() 返回arr中每一项的可变借用,遍历后arr可以继续使用。
如果还需要索引,那么就要使用iterator的enumerate方法,这个方法将当前迭代器封装成迭代元素是包含索引和元素的元组的迭代器,如下:
let arr = vec![1, 2, 3]; for (i, v) in arr.into_iter().enumerate() { println!("{} {}", i, v); }let arr = vec![1, 2, 3]; for (i, v) in arr.iter().enumerate() { println!("{} {}", i, v); }let mut arr = vec![1, 2, 3]; for (i, v) in arr.iter_mut().enumerate() { println!("{} {}", i, v); }

在Rust中还有一个常用的只返回索引的遍历方法:
for i in 1..4{ println!("{}",i); }

输出:
1
2
3
可以看到 beg..end 是一个左闭右开的区间,不包含end。
并发任务分发
在实际项目开发中,经常会需要将一组任务数据分发给不同的goroutine或thread来并发执行。
Golang
我们先来看在Golang中常见的错误写法
func slice_trap_wrong() {in := []int{1, 2, 3} for _, b := range in { go func() { fmt.Println("job", b) }() }}func main() {slice_trap_wrong() select {}}

运行得到结果:
job 3 job 3 job 3 fatal error: all goroutines are asleep - deadlock! goroutine 1 [select (no cases)]: main.main() /home/repl/904b2209-3e69-479f-a530-1954e1cf59cd/main.go:25 +0x25 exit status 2** Process exited - Return Code: 1 **

为了保证程序中的goroutine都能完全执行,所以main主goroutine不能提前退出,这里我们使用 select {}永久阻塞当前main函数,所以最终会报fatal error: all goroutines are asleep - deadlock!的错误。
我们忽略最后的error信息,可以看到三个goroutine执行do任务后,打印出来的数据都是3。原因在于for _, b := range in作用域中的b是同一个变量,所以程序中新创建的三个goroutine中闭包所关联的b都是同一个变量。根据GPM模型,这三个goroutine在创建后并不一定会立刻执行,等for _, b := range in遍历完后,此时b的数值是3,等这三个goroutine真正开始执行的时候,因为它们执行的是同一个b,所以得到的就是b最后的数值3.
知道错误原因后也好解决,有下面两种方式:
  1. 将任务数据作为入参传入,因为golang中参数传递都是按值传递的,所以进入到goroutine中的b已经是复制之后的一个新的变量
    func slice_trap_right() { in := []int{1, 2, 3} for _, b := range in { go func(b int) { fmt.Println("job", b) }(b) } }

2.在传入goroutine的闭包之前手动进行一次复制
go func slice_trap_right() { in := []int{1, 2, 3} for _, b := range in { b:=b go func() { fmt.Println("job", b) }() } }

Rust
接下来我们看下Rust中错误的写法:
let arr = vec![1, 2, 3]; let mut t_arr = vec![]; for a in arr { t_arr.push(thread::spawn(|| println!("{}", a))); }// 等待所有线程执行结束 for t in t_arr { t.join().unwrap(); }

编译器直接报错,因为thread的生命周期超过了 for a in arr中 a 的生命周期 :
error[E0373]: closure may outlive the current function, but it borrows `a`, which is owned by the current function --> src/main.rs:16:34 | 16 |t_arr.push(thread::spawn(|| println!("{}", a))); |^^- `a` is borrowed here || |may outlive borrowed value `a` | note: function requires argument type to outlive `'static` --> src/main.rs:16:20 | 16 |t_arr.push(thread::spawn(|| println!("{}", a))); |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: to force the closure to take ownership of `a` (and any other referenced variables), use the `move` keyword | 16 |t_arr.push(thread::spawn(move || println!("{}", a))); |++++For more information about this error, try `rustc --explain E0373`. error: could not compile `channel` due to previous error

Rust错误提示中给出了解决方法,使用move关键字,让闭包获取a的所有权:
let arr = vec![1, 2, 3]; let mut t_arr = vec![]; for a in arr { println!("{:p}", &a); t_arr.push(thread::spawn(move || println!("{} {:p}", a, &a))); }for t in t_arr { t.join(); }

输出结果
0x7ffe3a4398d4 0x7ffe3a4398d4 1 0x7f68b4b87b8c 0x7ffe3a4398d4 2 0x7f68b4986b8c 3 0x7f68b4785b8c

可以看到最终thread中运行的 a 和for a in arr中的 a 不是同一个,而且不同thread中的 a 也都是不同的变量。
我们再把arr中成员的原始地址打印出来:
let arr = vec![1, 2, 3]; let mut t_arr = vec![]; for a in arr.iter(){ println!("{:p}",a); } for a in arr { println!("{:p}", &a); t_arr.push(thread::spawn(move || println!("{} {:p}", a, &a))); }for t in t_arr { t.join(); }

得到输出:
0x5633d313ead0 0x5633d313ead4 0x5633d313ead8 0x7ffdbb7d2cc4 0x7ffdbb7d2cc4 1 0x7fee6d482b8c 0x7ffdbb7d2cc4 2 0x7fee6d281b8c 3 0x7fee6d080b8c

可以知道arr中的成员首先转移给了a,然后在执行move后再次进行了复制。
我们再试下结构体,不 derive Copy trait。
`pub struct People {
pub age: i32,

}`
编写测试代码如下:
let arr = vec![People { age: 1 }, People { age: 2 }, People { age: 3 }]; let mut t_arr = vec![]; for a in arr.iter() { println!("{:p}", a); } for a in arr { println!("{:p}", &a); t_arr.push(thread::spawn(move || println!("{} {:p}", a.age, &a))); }for t in t_arr { t.join(); }

得到输出
0x555c64ec9ad0 0x555c64ec9ad4 0x555c64ec9ad8 0x7ffdc5766474 0x7ffdc5766474 1 0x7f0b6f4b1b8c 0x7ffdc5766474 2 0x7f0b6f2b0b8c 3 0x7f0b6f0afb8c

可以看到arr中的People变量也是执行了两次移动。
如果项目中数据结构体比较大,执行多次移动不可接受的话,可以使用下面方式通过Arc进行优化。
Arc优化
Arc 是一种线程安全的引用计数指针,可以安全的在线程之前传递。
let arr = vec![ Arc::new(People { age: 1 }), Arc::new(People { age: 2 }), Arc::new(People { age: 3 }), ]; let mut t_arr = vec![]; for a in arr.iter() { // 执行复制,实际只是增加了strong引用计数 let a = Arc::clone(a); t_arr.push(thread::spawn(move || { // 通过 Arc::strong_count 可以得到Arc中的strong引用计数 println!("in thread {} count:{}", a.age, Arc::strong_count(&a)) })); }for t in t_arr { t.join().unwrap(); }// 线程执行结束后,内部的a被drop掉,实际只是减少了strong引用计数 for a in arr { println!("final: {} count:{}", a.age, Arc::strong_count(&a)); }

输出
in thread 1 count:2
in thread 3 count:2
in thread 2 count:2
final: 1 count:1
final: 2 count:1
final: 3 count:1
循环永动机
如果我们在遍历数组的同时修改数组的元素,能否得到一个永远都不会停止的循环呢?
我们先来看golang版本
Golang
这个例子来自 Go 语言for 和 range 实现
func main() { arr := []int{1, 2, 3} for _, v := range arr { arr = append(arr, v) } fmt.Println(arr) }$ go run main.go 1 2 3 1 2 3

上述代码的输出意味着循环只遍历了原始切片中的三个元素,我们在遍历切片时追加的元素不会增加循环的执行次数,所以循环最终还是停了下来。那么为什么没有一直执行下去呢?golang对于 for range 在真正生成机器码之前进行了转换,会先获取输入arr的长度保存起来,然后执行经典的三段式for循环,具体可以看 Go 语言for 和 range 实现 里dravness大佬的讲解,这里就不再细说。
Rust
我们看下rust中要实现上面的循环永动机:
let mut arr= vec![1,2,3]; for a in arr.iter() { println!("{}", a); arr.push(4); }

rust报错,由于rust的所有权安全机制,同时只能有一个mut的借用,并且此时也不能有其它的只读借用。
error[E0502]: cannot borrow `arr` as mutable because it is also borrowed as immutable --> src/main.rs:41:9 | 39 |for a in arr.iter() { |---------- || |immutable borrow occurs here |immutable borrow later used here 40 |println!("{}", a); 41 |arr.push(4); |^^^^^^^^^^^ mutable borrow occurs here

如果在真实项目中有这种基于当前Vector中数据生成新数据的需求,例如在DFS或BFS遍历的过程中,就要根据把当前父节点的子节点加入到遍历的数组中,可以这么写
let mut arr = vec![1, 2, 3]; let mut i = 0; let len = arr.len(); while i < len { arr.push(arr[i]); i += 1; }

注意上面的代码中也是预先计算当前arr的长度,如果使用下面的写法就会变成无限循环。
let mut arr = vec![1, 2, 3]; let mut i = 0; while i < arr.len() { arr.push(arr[i]); i += 1; }

神奇的指针
Golang
Golang例子同样来自于 Go 语言for 和 range 实现
func main() { arr := []int{1, 2, 3} newArr := []*int{} for _, v := range arr { newArr = append(newArr, &v) } for _, v := range newArr { fmt.Println(*v) } }$ go run main.go 3 3 3

很多人以为返回的是1,2,3,原因在于他们以为for range 中的v是在每个循环内部临时生成的,他们以为是这样的
for i:=0; i

实际上是
v:=0 for i:=0; i

详细的可以去看原文以及下面的评论,大家也可以把arr中和newArr中的遍历的地址打印出来就明白了。
Rust
我们使用 for a in arr.iter() 直接获取arr中每一个遍历的不可变借用,所以程序中new_arr可以按照预期正常打印
let arr: Vec = vec![1, 2, 3]; let mut new_arr: Vec<&i32> = vec![]; for a in arr.iter() { new_arr.push(a); } for a in new_arr { println!("{}", *a); }

输出
1
2
3
需要注意的是,如果使用下面的方式编写,会编译报错
let arr: Vec = vec![1, 2, 3]; let mut new_arr: Vec<&i32> = vec![]; for a in arr { new_arr.push(&a); } for a in new_arr { println!("{}", *a); }

原因在于for a in arr 执行结束后,a就被drop掉了,这里的a的声明周期不足以支撑到 for a in new_arr:
error[E0597]: `a` does not live long enough --> src/main.rs:60:22 | 60 |new_arr.push(&a); |^^ borrowed value does not live long enough 61 |} |- `a` dropped here while still borrowed 62 |for a in new_arr { |------- borrow later used here

另外我们可以再做个测试样例看for a in arr中的a是不是在每次迭代中创建了一个新变量还是复用一个变量:
let arr: Vec = vec![1, 2, 3]; for a in arr.iter(){ println!("{:p}",a); } for a in arr { println!("{:p}",&a); }

我们首先通过 arr.iter()得到arr中每个元素的不变借用,也就是元素地址打印出来,然后再把 for a in arr 中的每次迭代中的a的地址打印出来,如下:
0x563932220b10 0x563932220b14 0x563932220b18 0x7fff5e40c2e4 0x7fff5e40c2e4 0x7fff5e40c2e4

for a in arr中的打印看到地址都一样,说明复用了同一个变量,跟golang中的 for range 类似。
遍历 map
这里简单对比下两种语言的遍历方式,更加详细的map的对比会有专门一期文章来讲。
Golang
scores := make(map[string]int) scores["Yello"] = 50 scores["Blue"] = 10 for k, v := range scores { fmt.Println(k, v) }

Rust
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); }

参考
Go 语言for 和 range 实现 https://draveness.me/golang/d...
【Let's Go Rust 系列之for对比】Let's Go Rust 系列之for对比
文章图片

    推荐阅读