Rust的闭包

文章目录

  • Rust的闭包
    • 创建闭包
    • 闭包捕获当前环境中的变量
    • 闭包作为函数参数

Rust的闭包

几乎每一种比C语言高级的语言都有闭包。Rust也同样支持闭包。闭包有一个很通俗的名称:匿名函数,它有着如下优点:

  • 创建闭包不用为函数取名,方便快捷
  • 闭包可以捕获调用者作用域中的值
  • 闭包可以被保存进变量或作为参数传递给其他函数

创建闭包

Rust创建闭包的语法很简单:|参数列表| -> 返回类型 {代码段}。用两个竖线将函数参数包裹起来,后面接返回值的类型,最后是用花括号包裹的代码段就可以了。如:

fn main() {
    let closure = |a: i32| -> i32 {
        println!("a={}", a);
        a
    };
    println!("closure return {}", closure(10));
}

如果没有返回值,返回类型可以被省略:

fn main() {
    let closure = |a: i32| {
        println!("a={}", a);
    };
    
    closure(10);
}

如果代码段只有一行,花括号也可以被省略:

fn main() {
    let closure = |a: i32| println!("a = {}", a);
    closure(10);
}

Rust还可以根据闭包的调用,自动推到出闭包参数的类型。如我们只写一个闭包,省略参数,而不去调用它:

fn main() {
    let closure = |a| println!("a = {}", a);
}

此时,编译会报错:

error[E0282]: type annotations needed
 --> src\main.rs:2:20
  |
2 |     let closure = |a| println!("a = {}", a);
  |                    ^ consider giving this closure parameter a type

error: aborting due to previous error

添加上调用,Rust就可以自动推导了:

fn main() {
    let closure = |a| println!("a = {}", a);
    closure(10);
}

不过,这个推导只在第一次调用时进行,如果有两次调用,但参数的类型不一致:

fn main() {
    let closure = |a| println!("a = {}", a);
    closure(10);
    closure(1.0);
}

当第一次调用时,a被推导为整数,而第二次调用时编译器已经知道a的类型,再传入了浮点数,编译器就会报错:

error[E0308]: mismatched types
 --> src\main.rs:4:13
  |
4 |     closure(1.0);
  |             ^^^ expected integer, found floating-point number

error: aborting due to previous error

闭包捕获当前环境中的变量

闭包的一个重要的特性就是它可以捕获当前环境中的变量,如

fn main() {
    let num = 5;
    let closure = |a| println!("a={}, num={}", a, num);
    closure(10);
}

这里的闭包,不只用到了闭包的参数,还用到了一个外部的变量num。

注意,闭包捕获外部变量可不是一件简单的事情,它并不像函数传递参数那样。如下面的例子:

fn main() {
    let mut num = 5;
    let closure = |a| println!("a={}, num={}", a, num);

    closure(10);
    num = 10;
    closure(10);
} 

闭包中使用可变的变量num,然后闭包执行后,外部再修改num的值,然后再一次调用闭包。简单的理解这段代码,以为是第一次输出num=5,第二输出num=10。但是,事情却和想像中的不同,这段代码并不能通过编译:

error[E0506]: cannot assign to `num` because it is borrowed
 --> src\main.rs:6:5
  |
3 |     let closure = |a| println!("a={}, num={}", a, num);
  |                   --- borrow of `num` occurs here --- borrow occurs due to use in closure
...
6 |     num = 10;
  |     ^^^^^^^^ assignment to borrowed `num` occurs here
7 |     closure(10);
  |     ------- borrow later used here

error: aborting due to previous error; 1 warning emitted

编译器说闭包已经借用了num,取得了num的所有权,外部不能再改变它了。我不理解为什么借用的所有权没有归还,问群里的大佬,大佬说把第6行和第7行换一下位置就可以了:

fn main() {
    let mut num = 5;
    let closure = |a| println!("a={}, num={}", a, num);

    closure(10);
    closure(10);
    num = 10;
} 

虽然这段代码在逻辑上和这一段代码不同了,但它确实通过了编译。原来闭包并不像函数那样,调用完了就完了,因为闭包绑定到了变量closure,在离开closure的作用域之前,闭包一直有效,借用不会归还。第一段代码num=10后面还有对闭包的调用,所以闭包并没有被销毁,而第二段代码第二次调用闭包后,就不再使用了,因此执行完了第二次调用闭包就被销毁了,num=10才能正常执行。

为了实现第一段代码的逻辑,一个闭包是不能完成的,需要两个闭包才能实现:

fn main() {
    let mut num = 5;
    let closure = |a| println!("a={}, num={}", a, num);
    closure(10);
    
    num = 10;
    
    let closure = |a| println!("a={}, num={}", a, num);
    closure(10);    
} 

第一个闭包执行完后被销毁,num=10可以被执行,再使用第二个闭包(虽然两个一模一样),第二个闭包再捕获的变量就是改变以后的了,可以到达想像中的效果。

群里的大佬们开始讨论起来闭包捕获外部变量的高级知识,弄得我像听天书一看,于是上网查阅资料,发现一知乎大神讲得的非常全面。我在这里仅作一下总结,想了解更底层细节的可以直接去看大神的文章:https://www.jianshu.com/p/ea1b96cbf0a1。

闭包捕获变量有三种方式,是能过三个特性实现的:

  • Fn:如果闭包只是对捕获变量的非修改操作,闭包捕获的是&T类型,闭包按照Fn trait方式执行,闭包可以重复多次执行
  • FnMut:如果闭包对捕获变量有修改操作,闭包捕获的是&mut T类型,闭包按照FnMut trait方式执行,闭包可以重复多次执行
  • FnOnce:如果闭包会消耗掉捕获的变量,变量被move进闭包,闭包按照FnOnce trait方式执行,闭包只能执行一次

这三种方式是编译器根据闭包中的行为自动选择的,我们并不能指定它。此外,还可以在闭包前添加move关键字,告诉编译器复制一份变量到闭包中,这样,闭包就不再借用外部变量了:

fn main() {
    let mut num = 5;
    let closure = move |a| println!("a={}, num={}", a, num);
    closure(10);    
    num = 10;
    closure(10);    
} 

此时,闭包中的num与外部的num不再是同一个变量,外部对变量的修改也不再对闭包产生影响。只是我不太明白,为什么是move而不是CopyClone呢。

闭包作为函数参数

上面例子的闭包被保存在变量closure中,当然,它可以作为函数参数进行传递。不过,这似乎有些麻烦,比如:

fn main() {
    let closure = |a| println!("a = {}", a);
    call_closure(closure);
}

fn call_closure(closure) {
    closure(10);
}

写到这自然会想到,call_closure函数的参数closure是什么类型呢?

事实上,闭包不属于任何类型,它的本质是:特性。要想使用闭包作为函数参数,应该这么写:

fn main() {
    let closure = |a| println!("a = {}", a);
    call_closure(closure);
}

fn call_closure(closure: impl Fn(i32)) {
    closure(10);
}

closure参数指定的不是参数的类型,而是指定参数实现的特性。可以进一步使用泛型来作为函数参数,它同样支持传递闭包(还不懂泛型的小朋友可以参考我关于泛型的博文:《Rust的泛型》):

fn main() {
    let num = 5;
    let closuer = move |a| println!("a={}, num={}", a, num);
    call_closuer(closuer);
}

fn call_closuer(closuer: F)
where F: Fn(i32) {
    closuer(10);
}

与直接调用不同,使用了泛型以后,我们就有了选择特性的手段:特性绑定。通过特性绑定,我们可以需要自己决定使用哪种变量捕获方式。此时,就需要我们明确的知道自己写的闭包到底是通过FnFnMut还是FnOnce的方式。这不是可以随意选择的,必须和创建闭包时编译器选择的一样,所以,还是需要牢记这三个特性的使用条件:

  • Fn:如果闭包只是对捕获变量的非修改操作,闭包捕获的是&T类型,闭包按照Fn trait方式执行,闭包可以重复多次执行
  • FnMut:如果闭包对捕获变量有修改操作,闭包捕获的是&mut T类型,闭包按照FnMut trait方式执行,闭包可以重复多次执行
  • FnOnce:如果闭包会消耗掉捕获的变量,变量被move进闭包,闭包按照FnOnce trait方式执行,闭包只能执行一次

你可能感兴趣的:(Rust语言学习笔记,rust)