rust学习之闭包closure

本文详细解释了闭包在Rust中的概念,包括其定义、特性(如Fn、FnOnce、FnMut)、如何捕获变量(所有权转移、可变借用和不可变借用),以及在函数参数和返回值中的使用。特别关注了Rust中trait和implTrait在闭包中的应用。
摘要由CSDN通过智能技术生成

闭包的定义

闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值,例如:

fn main() {
   let x = 1;
   let sum = |y| x + y;

    assert_eq!(3, sum(2));
}

上面的代码展示了非常简单的闭包 sum,它拥有一个入参 y,同时捕获了作用域中的 x 的值,因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和为3

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

闭包与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,...|, 闭包的形式定义为:

|param1, param2,...| {

语句1;

语句2;

返回表达式

}

如果只有一个返回表达式的话,定义可以简化为:|param1| 返回表达式 

  • 闭包中最后一行表达式返回的值,就是闭包执行后的返回值
  • let action = ||... 只是把闭包赋值给变量 action,并不是把闭包执行后的结果赋值给 action,因此这里 action 就相当于闭包函数,可以跟函数一样进行调用:action()
  • 闭包并不会作为 API 对外提供,无需标注参数和返回值的类型。 为了增加代码可读性,有时可以显式地给类型进行标注 。如果你只对参数进行了声明而没有使用,则需要显式标注类型

虽然闭包的类型推导很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型

let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5); //报错

在 s 中,编译器为 x 推导出类型 String,但是紧接着 n 试图用 5 这个整型去调用闭包,跟编译器之前推导的 String 类型不符,因此报错

结构体中的闭包

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    query: T,
    value: Option<u32>,
}

impl<T> Cacher<T>
where
    T: Fn(u32) -> u32,
{
    fn new(query: T) -> Cacher<T> {
        Cacher {
            query,
            value: None,
        }
    }

    // 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.query)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

Fn(u32) -> u32 是一个特征,用来表示 T 是一个闭包类型,该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。query 的类型是 T,该类型必须实现了相应的闭包特征 Fn(u32) -> u32

函数参数中的闭包

闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn 特征也有三种:

1、转移所有权:

fn fn_once<F>(func: F)  //func就是闭包,其类型F满足如下特征
where
    F: FnOnce(usize) -> bool, //正确:F: FnOnce(usize) -> bool + Copy,
{
    println!("{}", func(3)); 
    println!("{}", func(4));  //F只实现了FnOnce特征,要转移所有权,因此只能使用一次func
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})  //{z == x.len()}就是func 入参式usize类型,返回值式bool
}

//输出 
true
false

FnOnce 特征的闭包在调用时会转移所有权,因为 F 没有实现 Copy 特征,所以会报错,那么我们添加一个约束,实现Copy 闭包就能顺利运行。如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字:

use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();

2、可变借用:

FnMut特征以可变借用的方式捕获了环境中的值,除了申明变量本身是可变的,还需要把该闭包声明为可变类型,以便在闭包内部捕获可变借用,不然会编译出错:

fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str); //正确:let mut update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

也可以修改成如下形式:

fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: FnMut(&'a str)>(mut f: F)  {
    f("hello")
}

这段代码中update_string没有使用mut关键字修饰,但exec(mut f: F)表明我们的exec接收的是一个可变类型的闭包。事实上,FnMut只是trait的名字,声明变量为FnMut和要不要mut没啥关系,FnMut是推导出的特征类型mut是rust语言层面的一个修饰符,用于声明一个绑定是可变的。Rust从特征类型系统和语言修饰符两方面保障了我们的程序正确运行。在使用FnMut类型闭包时需要捕获外界的可变借用,因此我们常常搭配mut修饰符使用,但二者是相互独立的。这段代码的流程是:在main函数中,首先创建了一个可变的字符串s,然后定义了一个可变类型闭包update_string,该闭包接受一个字符串参数并将其追加到s中。接下来调用了exec函数,并将update_string闭包的所有权移交给它。最后打印出了字符串s的内容,update_string没有实现Copy特征,因此也只能调用一次,但并不是所有闭包都没有实现Copy特征,闭包自动实现Copy特征的规则是:只要闭包捕获的类型都实现了Copy特征的话,这个闭包就会默认实现Copy特征。例如取得的是字符串字面量s的不可变引用,则是能Copy的。而如果拿到的是s的所有权或可变引用,都是不能Copy:
 

//不可变引用
let s = String::new();
let update_string =  || println!("{}",s);

// 拿所有权
let s = String::new();
let update_string = move || println!("{}", s);

exec(update_string);
// exec2(update_string); // 不能再用了

// 可变引用
let mut s = String::new();
let mut update_string = || s.push_str("hello");
exec(update_string);
// exec1(update_string); // 不能再用了

3、不可变借用:

Fn 特征以不可变借用的方式捕获环境中的值 

fn main() {
    let s = "hello, ".to_string();

    let update_string =  |str| println!("{},{}",s,str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: Fn(String) -> ()>(f: F)  { 
    f("world".to_string())
}

因为无需改变 s,因此闭包中只对 s 进行了不可变借用,那么在就在exec中将其标记为 Fn 特征,而如果在上一例中,将fn exec<'a, F: FnMut(&'a str)>(mut f: F)  { 中的FnMut(&'a str)替换成Fn(&'a str)就会报错,因为闭包实现的是 FnMut 特征,需要的是可变借用,但是在 exec 中却给它标注了 Fn 特征,因此产生了不匹配。

三种Fn关系:

一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们move 本身强调的就是如何捕获变量。例如,下面的闭包中使用了 move 关键字,所以我们的闭包捕获了它,但是由于闭包对 s 的使用仅仅是不可变借用,因此该闭包实际上实现了 Fn 特征。

fn main() {
    let s = String::new();

    let update_string =  move || println!("{}",s);

    exec(update_string);
}

fn exec<F: FnOnce()>(f: F)  { //也可以写成:fn exec<F: Fn()>(f: F)  {
    f()
}

因此,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
  • 不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

关于第二条规则,有如下示例:

fn main() {
    let mut s = String::new();

    let update_string = |str| -> String {s.push_str(str); s };

    exec(update_string);
}

fn exec<'a, F: FnMut(&'a str) -> String>(mut f: F) {
    f("hello");
}

 此例中,闭包从捕获环境中移出了变量 s 的所有权,因此这个闭包仅自动实现了 FnOnce,未实现 FnMut 和 Fn。从特征约束能看出来 Fn 的前提是实现 FnMutFnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce,在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。

函数返回值中的闭包 

Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32 就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。因此不能直接使用用Fn(i32) -> i32 特征来代表 |x| x + num 。需使用impl Fn(i32) -> i32 的返回值形式,说明我们要返回一个闭包类型,它实现了 Fn(i32) -> i32 特征,但这种impl Trait 的返回方式只能返回同样的类型,对于不同的闭包类型,需用特征对象,Box 的方式即可实现:

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
    let num = 5;

    if x > 1{
        Box::new(move |x| x + num)
    } else {
        Box::new(move |x| x - num)
    }
}

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值