zoukankan      html  css  js  c++  java
  • Rust点滴: 闭包那点事儿

    Rust点滴: 闭包那点事儿

    概述

    我们常常需要回调函数的功能, 需要函数并不是在创建时执行, 而是以回调的方式, 在需要的时候延迟执行. 并且, 常常需要在函数中获取环境中的一些信息, 又不需要将其作为函数参数传入. 这种应用场景就需要闭包这一工具了.

    闭包是持有外部环境变量的函数. 所谓外部环境, 就是指创建闭包时所在的词法作用域.

    闭包的语法: |params| {expr}

    其中params表示向闭包中传递的参数, 类似于函数参数. 可以显式指定类型, 也可由编译器自动推导.

    expr表示闭包中的各种表达式, 其返回值类型作为为闭包的返回值类型.

    let a = "hello";
    let print = || {println!("{:?}", a);};
    print();
    复制代码

    上面的代码段创建了一个闭包, 打印环境变量a的值, 没有传入参数, 返回值类型为().

    分类

    使用环境变量的方式

    Rust中的闭包, 按照对捕获变量的使用方式, 将闭包分为三个类型: FnFnMutFnOnce. 其中Fn类型的闭包, 在闭包内部以共享借用的方式使用环境变量; FnMut类型的闭包, 在闭包内部以独占借用的方式使用环境变量; 而FnOnce类型的闭包, 在闭包内部以所有者的身份使用环境变量. 由此可见, 根据闭包内使用环境变量的方式, 即可判断创建出来的闭包的类型.

    注意, 对于Copy类型的环境变量, 如果以传值的方式使用, 其默认的闭包类型是Fn, 而非FnOnce, 而对非Copy的环境变量, 其闭包类型只能是FnOnce.

    闭包中环境变量最终的捕获方式 (即, 是借用, 是复制, 还是转移所有权), 还与环境变量本身的语义, 以及闭包是否强制获取环境变量的所有权有关.

    举例说明:

    #![feature(fn_traits)]
    fn main() {
        let mut a = 1;
        let mut print = || {
            &a;
        };
    
        print.call_once(()); // OK
        print.call_mut(()); // OK
        print.call(()); // OK
    }
    复制代码
    #![feature(fn_traits)]
    fn main() {
        let mut a = 1;
        let mut print = || {
            &mut a;
        };
    
        print.call_once(()); // OK
        print.call_mut(()); // OK
        print.call(()); // error, the requirement to implement `Fn` derives from here
    }
    复制代码
    #![feature(fn_traits)]
    fn main() {
        let mut a = 1;
        let mut print = || {
            a;
        };
    
        print.call_once(()); // OK
        print.call_mut(()); // OK
        print.call(()); // OK
    }
    复制代码

    最后这个比较神奇, 印象中以为Copy和非Copy的环境变量, 而实际上创建的闭包由于环境变量都是Copy的, 默认实现了Fn. 如果是非Copy的环境变量, 则只能实现FnOnce.

    #![feature(fn_traits)]
    fn main() {
        let mut a = "str".to_string();
        let mut print = || {
            a;
        };
    
        print.call_once(()); // OK
        print.call_mut(()); // error, the requirement to implement `FnMut` derives from here
        print.call(()); // error, the requirement to implement `Fn` derives from here
    }
    复制代码

    是否强制move

    在闭包的管道符前面加上move关键字, 会强制以传值的方式捕获变量. 至于是复制还是移动, 则与环境变量类型的语义有关. 我们知道, 一个类型实现Copy, 即为复制语义. 在作为右值使用时会将值按位复制. 而未实现Copy的类型即为移动语义, 作右值使用时会转移所有权.

    举个例子:

    // 没有强制move, 不强制按值捕获变量
    fn main() {
        let mut a = 1;
        let print = || {
            &a;
        };
        let aa = &mut a; // 这里编译报错, mutable borrow occurs here
        print();
    }
    复制代码

    之所以声明可变借用aa编译报错, 是因为创建闭包时, 由于是使用可变借用, 因此默认按可变借用捕获环境变量a. 我们知道, 可变借用和不可变借用不能同时使用.

    // 强制move, 按值捕获变量
    fn main() {
        let mut a = 1;
        let print = move || { // 这里添加move, 强制按值捕获变量
            &a;
        };
        let aa = &mut a; // 这里不报错, 因为闭包中复制了a的值
        print();
    }
    复制代码

    环境变量的语义

    虽然环境变量的类型的语义不影响捕获方式, 但却会影响创建出来的闭包的性质. 如果所有捕获的环境变量均为Copy, 则闭包为Copy, 否则闭包为非Copy, 需要移动.

    举个例子:

    // 环境变量是Copy, 则闭包是Copy
    fn main() {
        let mut a = 1;
        let print = move || {
            a;
        };
        let print2 = print; // 因为闭包只捕获了a, 而a是i32是Copy的, 所以print是Copy的
        print(); // 这里没有发生所有权转移, 是按位复制, print仍然可用
        print2();
    }
    复制代码
    // 环境变量非Copy, 则闭包非Copy
    fn main() {
        let mut a = 1;
        let mut s = "str".to_string();
        let print = move || {
            a;
            s;
        };
        let print2 = print;
        print(); // 这里就要报错了, value used here after move
        print2();
    }
    复制代码

    用法

    闭包的用法在<<Rust编程之道>>这本书中有比较详细的说明, 主要有两种用法, 作为函数参数, 作为函数返回值. 其中, 作为函数返回值时, 需要注意FnOnce需要特殊处理, Rust会将其封装成FnBox, 从而解决闭包trait对象在解引用时的拆箱问题.

    其他

    ##闭包的逃逸性 根据一个闭包是否会逃逸到创建该闭包的词法作用域之外, 可以将闭包分为非逃逸闭包和逃逸闭包.

    这二者最根本的区别在于, 逃逸闭包必须复制或移动环境变量. 这是很显然的, 如果闭包在词法作用域之外使用, 而其如果以引用的方式获取环境变量, 有可能引起悬垂指针问题.

    逃逸闭包的类型声明中, 需要加一个静态生命周期参数'static.

    // 非逃逸闭包, 不按值捕获环境变量也可以编译通过
    fn main() {
        let a = 1;
        let c: Box<Fn()> = Box::new(|| {
            &a;
        });
    }
    复制代码
    // 显式声明类型为逃逸闭包, 不按值捕获环境变量会编译失败
    fn main() {
        let a = 1;
        let c: Box<Fn()+'static> = Box::new(|| {
            &a; // error, borrowed value does not live long enough
        });
    }
    复制代码
    // 显式声明类型为逃逸闭包, 按值捕获环境变量, 编译通过
    fn main() {
        let a = 1;
        let c: Box<Fn()+'static> = Box::new(move || {
            &a;
        });
    }
    复制代码

    高阶生命周期

    主要解决闭包参数中含有引用时的生命周期标注的问题. Rust通过高阶trait限定的for<>语法, 解决这一问题.

    总结

    闭包的几个关键点:

    • 闭包如何捕获环境变量: 与环境变量是否Copy, 是否强制move有关.
    • 闭包类型: 与环境变量是否Copy, 环境变量在闭包中的使用方式有关.
    • 闭包在何时使用环境变量: 涉及闭包的逃逸性, 逃逸闭包必须传值.

    参考资料

  • 相关阅读:
    java集合框架小结(初级版)
    [leetcode]Decode Ways
    [leetcode]Distinct Subsequences
    [leetcode]Longest Valid Parentheses
    [leetcode]Edit distance
    python+selenium浏览器调用(chrome、ie、firefox)
    Python爬虫入门(6):Cookie的使用
    Python进行RSA安装加密
    解决cron无法运行报错:FAILED to authorize user with PAM (Module is unknown)
    使用eclipse开发Java web应用
  • 原文地址:https://www.cnblogs.com/dream397/p/14190708.html
Copyright © 2011-2022 走看看