Go的闭包看你犯错,Rust却默默帮你排坑

2022-12-29 0 856

译者|钟会白眉林|晋兆雨

公司出品 | CSDN网志

Go的闭包看你犯错,Rust却默默帮你排坑

贝唐旋量群

旋量群(Closure)在这类C词汇中也被称作 Lambda 函数,是能加载其它函数内部函数的函数。通常多于函数内部的子函数就可以加载局部函数,所以旋量群这种两个函数内部的函数,在其本质上是将函数内部和函数内部相连接的公路桥。

在课堂教学之中,倘若他们须要统计数据两个函数被初始化的单次,最简单的形式是表述两个全局函数,每每最终目标函数被初始化时就将此函数加1,但全局函数会增添许多误为等难题,可靠性常常不能获得确保;而为初始化单次专门针对结构设计两个以算数的USB又太轻率了。

但透过旋量群就较为难同时实现算数机能,以Go词汇为例具体内容标识符及注解如下表所示:

package main

import (

“fmt”

)

funcSomeFunc()func() int { // 建立两个函数,回到两个旋量群,旋量群每天初始化函数会对函数内部函数展开加总

varCallNum int = //函数初始化单次,系函数内部函数,内部封禁,若要函数被初始化时展开加总

returnfunc() int { // 回到两个旋量群

CallNum++ //对value展开加总

//同时实现函数具体内容方法论

returnCallNum// 回到内部函数value的值

}

}

funcmain() {

accumulator := SomeFunc() //使用accumulator函数接收两个旋量群

// 加总算数并打印

fmt.Println(“The first call CallNum is “, accumulator()) //运行结果为:The first call CallNum is 1

// 加总算数并打印

fmt.Println(“The second call CallNum is “, accumulator()) //运行结果为:The second call CallNum is 2

}

运行结果为:

The first call CallNum is 1

The second call Cal

可以看到他们透过旋量群即没有暴露CallNum这个函数,又同时实现了为函数算数的目的。

Go的闭包看你犯错,Rust却默默帮你排坑

Goroutine+旋量群却出了莫名其妙的BUG

在Go词汇中,旋量群所依托的匿名函数也是Goroutine所经常用到的方案之一,但这两者一结合却难出现极难排查的BUG,接下来我把出现难题的标识符简化一下,请读者们来看下面这段标识符:

import (

“fmt”

“time”

)

funcmain() {

        tests1ice := []int{1, 2, 3, 4, 5}

for _, v := range tests1ice {

gofunc() {

fmt.Println(v)

                }()

        }

        time.Sleep(time.Millisecond)

}

这段标识符的方法论不难看懂,其最终目标是透过Goroutine将1,2,3,4,5乱序输出到屏幕上,但最终执行结果却如下表所示:

5

5

5

3

5

成功: 进程退出标识符 0.

也是多于大多数情况下多于5被输出出来了,1-4几乎没有什么机会登场,这里简要复述一下难题的排查过程,由于没有在Goroutine中对切片执行写操作,因此首先排除了内存屏障的难题,最终还是透过反编译查看汇编标识符,发现Goroutine打印的函数v,其实是地址引用,Goroutine执行的时候函数v所在地址所对应的值已经发生了变化,汇编标识符如下表所示:

for_, v := range tests1ice {

499224:       488d 05 f5 af 0000    lea    0xaff5(%rip),%rax        # 4a4220 

49922b:       48890424             mov    %rax,(%rsp)

49922f:       e8 8c 3a f7 ff          callq  40ccc

499234:       488b 4424 08          mov    0x8(%rsp),%rax

499239:       4889442448          mov    %rax,0x48(%rsp)

49923e:       31 c9                   xor    %ecx,%ecx

499240:       eb 3e                   jmp    499280 0xc0>

499242:       48894c 2418          mov    %rcx,0x18(%rsp)

499247:       488b 54 cc 20          mov    0x20(%rsp,%rcx,8),%rdx

49924c:       488910                mov    %rdx,(%rax)

                go func() {

49924f:       c7 0424 08 000000    movl   $0x8,(%rsp)

499256:       488d 15 f3 b7 0200    lea    0x2b7f3(%rip),%rdx        # 4c4a50

49925d:       48895424 08          mov    %rdx,0x8(%rsp)

499262:       4889442410          mov    %rax,0x10(%rsp)

499267: e8 543a fa ff callq 43ccc

可Goroutine中fmt.Println所处理的v,其实是v的地址中所对应的值。这也是产生这个BUG的基本原因。

找到了难题的原因,解决起来也就简单多了。

解决方案一:在参数形式向匿名函数传递值引用,具体内容标识符如下表所示: 

package main

import (

“fmt”

“time”

)

funcmain() {

tests1ice := []int{1, 2, 3, 4, 5}

for _, v := range tests1ice {

 w := v

gofunc(v int) {

fmt.Println(v)

}(v)

}

time.Sleep(time.Millisecond)

}

解决方案二:在初始化gorouinte前将函数展开值拷贝

package main

import (

“fmt”

“time”

)

funcmain() {

tests1ice := []int{1, 2, 3, 4, 5}

for _, v := range tests1ice {

w := v

gofunc() {

fmt.Println(w)

}()

}

time.Sleep(time.Millisecond)

}

总而言之只要传值就没事,而传地址引用就会出现难题。

Go的闭包看你犯错,Rust却默默帮你排坑

Rust的Lifetime检查如何排坑?

利用周末时间我想看看上述难题标识符在Rust的同时实现中是如何处理的,却有较为意外的收获,他们来看上述标识符的Rust同时实现:

use std::thread;

use std::time::Duration;

fn main() {

   let arr = [1, 2, 3, 5, 5];

for i in arr.iter() {

let handle = thread::spawn(move || {

        println!(“{}”, i);

    });

   }

    thread::sleep(Duration::from_millis(10));

}

但上述这段标识符编译都无法透过,原因是arr这个函数的生命周期错配。具体内容编译结果如下表所示:

error[E0597]: `arr` does not live long enough

–> hello16.rs:6:14

|

6 |for i in arr.iter() {

| ^^^

||

| borrowed value does not live long enough

| cast requires that `arr` is borrowed for `static`

13 | }

| – `arr` dropped here while still borrowed

error: aborting due to previous error; 1 warning emitted

他们刚刚提到过匿名函数其实是透过地址引用的形式来访问局部函数的,而地址引用也就对应Rust之中借用的概念,那么他们就可以推出来for i in arr.iter()中的 arr.iter()实际是对arr的借用,这个借用后的结果i被let handle = thread::spawn(move 中的move关键字强制转移走了,因此在handle线程离开作用域之后就被释放了,而下次迭代时arr函数由于lifetime的难题不能被编译器编译透过。

为了更简要的说明这个难题他们来看下面的标识符:

fn main() {

    {

let x;

        {

let y = 5;

            x = &y;// x借用y的值

        }

// y在这里已经被释放,因此借用y的x也不能透过lifetime检查

        println!(“x: {}”, x);

}

}

x借用y的值,如果在y的lifetime以外,再出现x的访问就会出现难题。如果想避免这个难题就不能再使用借用的机制,可以编译透过的标识符如下表所示:

use std::thread;

use std::time::Duration;

fn main() {

let arr = [1, 2, 3, 5, 5];

for i in arr.iter() {//这段标识符中i是对arr的借用

let handle = thread::spawn(move || {//move将j强制转移给了handle

        println!(“{}”, j);

    });//这里j超出lifetime就不会影响到i了

   }

thread::sleep(Duration::from_millis(10));

}

新添加的let j=i+1;是透过值拷贝的形式将i和j剥离了,因此j在被释放的时候就不会影响到arr的借用i了。

凡是编译器能发现的错误,都不会形成难题。透过这个Go词汇难题的排查,我对于Rust的函数生命周期检查机制有了更进一步的认识,不得不承认虽然Rust虽然难度很高,学起来较为劝退,但其安全词汇的名号真是所言不虚,他强制程序员必须按照正确的形式做事,如果能对Rust的机理做到知其然还能知其因此然,必将是个巨大的提升。

译者:钟会,CSDN网志专家,阿里云MVP、华为云MVP,华为2020年技术社区开发者之星。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务