关于golang:Lets-Go-Rust-系列之for对比

42次阅读

共计 7640 个字符,预计需要花费 20 分钟才能阅读完成。

前言

在 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<len(arr);i++{v := arr[i];
    newArr = append(newArr, &v)
}

实际上是

v:=0
for i:=0;i<len(arr);i++{v = arr[i];
    newArr = append(newArr, &v)
}

具体的能够去看原文以及上面的评论,大家也能够把 arr 中和 newArr 中的遍历的地址打印进去就明确了。

Rust
咱们应用 for a in arr.iter() 间接获取 arr 中每一个遍历的不可变借用,所以程序中 new_arr 能够依照预期失常打印

let arr: Vec<i32> = 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<i32> = 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<i32> = 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…

正文完
 0