• Rust闭包 - Fn/FnMut/FnOnce traits,捕获和传参


    Rust闭包: 是一类能够 捕获周围作用域中变量 的 函数


    |参数| {函数体}

    • 参数及返回值类型可推导,无需显示标注
    • 类型唯一性,确定后不可更改
    • 函数体为单个表达式时,{}可省略

    引言

    闭包区别于一般函数最大的特点就是,可以捕获周围作用域(不一定是当前同作用域,上级也可以)中的变量;当然,也可以选择啥都不捕获。

    let a = 0;
    
    // 一般函数
    // fn f1 () -> i32 {a} // 报错:fn中无法捕获动态环境变量
    
    // 闭包
    let f2 = || println("{}", a); // 闭包捕获&a
    let f3 = |a: i32|{}; // 闭包啥都没捕获,a只是个普通的形参
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里说的捕获不应该认为是像函数一样简单地传参,可以理解成闭包也是一种语法糖,它背后进行的操作要复杂的多,详细可参考文末相关资料[1]

    // 举个栗子,定义了以下闭包并调用
    let message = "Hello World!".to_string();
    let print_me = || println!("{}", message);
    
    print_me();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其实际进行的操作是这样:

    #[derive(Clone, Copy)]
    struct __closure_1__<'a> { // note: lifetime parameter
        message: &'a String, // note: &String, 下文会提到所谓的——捕获引用
    }
    
    impl<'a> Fn<()> for __closure_1__<'a> {
        // type Output = ();
        
        fn call(&self, (): ()) -> () {
            println!("{}", *self.message)
        }
    }
    
    let message = "Hello World!".to_string();
    let print_me = __closure_1__ { message: &message };
    
    
    Fn::call(&print_me, ());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1 分类 Fn / FnMut / FnOnce

    根据捕获变量进行的操作,Rust里的闭包实现的traits共三种
    注意!这里的因果关系,是捕获变量的操作 决定 闭包实现的形式

    • Fn : 可在不改变状态的情况下重复调用; 捕获变量的不可变引用(shared reference)或啥都不捕获
    • FnMut: 可改变状态,可重复调用; 捕获变量的可变引用(mutable reference
    • FnOnce: 只能调用一次,存在捕获的变量所有权转移被消耗
    // 闭包impl trait编译器会自动根据捕获操作推导,注释方便阅读
    let a = 0;
    // impl Fn()
    let f1 = || println("{}", a); // 捕获&a
    f1();
    f1();
    
    let mut b = 0;
    // impl FnMut()
    let mut f2 = || b+=1; // 捕获&mut b; 可能会有疑问为什么不需要解引用*b+=1, 参考相关资料[1]
    f2();
    f2();
    
    let c = "".to_string();
    // impl FnOnce()
    let f3 = || std::mem::drop(c);
    f3();
    //f3(); // 报错,f3只能调用一次,c所有权已经发生了转移并且消费了它
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2 关键词 move

    move将引用或可变引用捕获的任何变量转换为按值捕获的变量
    注意!闭包实现的traits是由对值进行的操作确定,而不是捕获值的方式;这意味即使闭包中捕获的是值,发生了所有权转移,它也可能是FnFnMut [2]

    (1) 实现Copy trait的对象,move时发生值拷贝

    let a = 0;
    // impl Fn()
    let f1 = move || println("{}", a); // 将捕获的不可变引用转换为值拷贝传递给闭包
    
    let mut b = 0;
    // impl FnMut()
    let mut f2 = move || b += 1;
    f2();
    f2();
    println("{}", b); // 因为闭包里是值拷贝,所以还是0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (2)未实现Copy trait的对象,move时发生所有权转移

    let a = "".to_string();
    // impl Fn()
    let f1 = move || println!("{}", a); // 环境中变量a对应值的所有权转移给了闭包a
    // 因为并未产生消耗,所以类型推导仍然是Fn,f1可以反复调用
    f1();
    f1();
    // println("{}", a); // 报错,使用了值已发生move的a
    
    let mut b = "".to_string();
    // impl FnMut()
    let mut f2 = move || {
    	b += "x";
    	println("{}", b);
    };
    f2(); // x
    f2(); // xx
    // println("{}", b); // 报错,使用了值已发生move的b
    
    let c = "".to_string();
    // impl FnOnce()
    let f3 = move || {
    	println("{}", c);
    	std::mem::drop(c); // 这边有没有move其实都一样,闭包drop未实现Copy的值,默认捕获的就是转移了所有权的环境变量
    };
    f3(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    (3)一些需要注意的点

    • 闭包中,若环境变量直接作为返回值,会以值的形式返回 [1]
    // 实现了Copy类型的数据
    let mut a = 0;
    // impl FnMut() -> i32
    let mut f1  = || {
    	a += 1// 捕获a引用
    	a // 没有";" 闭包类型推导的返回值是i32
    }; 
    f1();
    f1();
    println!("{}", a); // 2
    
    // 未实现Copy类型的数据
    let mut b = "".to_string();
    // impl FnOnce() -> String
    let mut f2 = || {
    	b += "x";  // 捕获所有权转移的b
    	b // 没有";" 返回所有权转移的b; 因为所有权发生转移,并作为返回值传递(消费),所以无法反复调用,故类型推导是FnOnce
    }
    f2();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 有些场景会对未实现Copy的变量触发隐式的move
      (没有找到相关的资料,暂且只能靠记忆)
    // std::mem::drop 参考之前的例子
    
    // path statement
    let a = "".to_string();
    // impl FnOnce() 
    let f1 = || {a;}; 
    
    // operation statement
    let b = "".to_string();
    // impl FnOnce()
    let f2 = || {b+"x";};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3 闭包作为参数传递

    Fn 继承自 FnMut 继承自 FnOnce
    在这里插入图片描述
    根据继承关系可以得到结论:

    • 当形参类型为Fn时,只能传递Fn
    • 当形参类型为FnMut时,可以传递 Fn, FnMut
    • 当形参类型为FnOnce,三种皆可

    定义:

    fn is_fn<F>(_: F) where F: Fn() -> () {}
    
    fn is_fn_mut<F>(_: F) where F: FnMut() -> () {}
    
    fn is_fn_once<F>(_: F) where F: FnOnce() -> () {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用:

    // impl Fn()
    let f1 = || {};
    
    let mut count = 0;
    // impl FnMut()
    let mut f2 = || count += 1;
    
    let s = "".to_string();
    // impl FnOnce()
    let f3 = || std::mem::drop(s);
    
    is_fn(f1);
    
    is_fn_mut(f1);
    is_fn_mut(&mut f2);
    
    is_fn_once(f1);
    is_fn_once(&mut f2);
    is_fn_once(f3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    注意!!!这里不能调用 is_fn_mut(f2)
    原因是闭包本身作为Fn*类型的数据,也是要考虑其本身Copy trait的实现:参考[3]

    • 若未发生捕获,或捕获的是值拷贝,或只进行了不可变的引用(shared reference),那么闭包本身也实现了Copy trait;
    // impl Fn(), 未捕获
    let fn_f1 = || {}; 
    is_fn(fn_f1);
    is_fn(fn_f1);
    
    // impl FnMut(), 捕获值拷贝
    let mut a = 0;
    let mut fnmut_f2 = move || count1 += 1; 
    is_fn_mut(fnmut_f2);
    is_fn_mut(fnmut_f2);
    
    // impl Fn(), 捕获不可变引用
    let b = 0;
    let fn_f3 = || println("", b);
    is_fn(fn_f3);
    is_fn(fn_f3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 若捕获的是可变引用(mutable reference),那么闭包本身则未实现Copy trait,需要注意所有权转移的可能
    fn is_fn_mut<F>(_: F) where F: FnMut() -> () {}
    
    let mut count = 0;
    // impl FnMut()
    let mut f2 = || count += 1;
    is_fn_mut(f2); // 仅调用一次没问题,但是此时f2所有权已经发生了move
    //is_fn_mut(f2); // 报错,使用了发生move的f2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    想要多次调用的话,需传递&mut f2&mut F也是实现了FnMut的,所以这里传递引用没有问题,参考[4]

    is_fn_mut(&mut f2);
    is_fn_mut(&mut f2);
    
    • 1
    • 2


    相关资料:
    [1] https://users.rust-lang.org/t/closure-capture-by-borrowing-is-not-a-regular-reference/55945/8
    [2] https://rustwiki.org/zh-CN/std/keyword.move.html
    [3] Additional implementors 其他实现者
    英 https://doc.rust-lang.org/core/marker/trait.Copy.html
    中 https://rustwiki.org/zh-CN/std/marker/trait.Copy.html
    [4] https://rustwiki.org/zh-CN/std/ops/trait.FnMut.html

  • 相关阅读:
    数据库MySQL(四):表中字段约束和外键约束
    《动手学深度学习 Pytorch版》 6.6 卷积神经网络
    会中收发材料,来试试“会议文件空间”吧
    Kotlin 开发Android app(十五):使用Broadcast收发广播
    外贸电商独立站的选品和运营
    广东电信和中兴开通首个50G PON万兆入企啦!
    div+css布局实现个人网页设计(HTML期末作业)
    Mybatis面试总结(1)
    怎么恢复移走的u盘数据?可以尝试这三种方法
    咕咕驴2022新版AI短视频一键换脸小程序源码/带流量主
  • 原文地址:https://blog.csdn.net/weixin_44539199/article/details/133770541