zoukankan      html  css  js  c++  java
  • Rust 中的 Closure

    闭包会捕获其环境
    闭包中可以使用x变量。
    
    fn main() {
        let x = 4;
    
        let equal_to_x = |z| z == x;
    
        println!("{:?}", x);
    
        let y = 4;
    
        assert!(equal_to_x(y));
    }
    而在函数中,则无法使用x变量。
    
    fn main() {
        let x = 4;
    
        fn equal_to_x(z: i32) -> bool { z == x }
    
        let y = 4;
    
        assert!(equal_to_x(y));
    }
    
    // 无法通过编译
    
    error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
    } closure form instead
     --> src/main.rs
      |
    4 |     fn equal_to_x(z: i32) -> bool { z == x }
      |  
    [root@bogon closure3]# cargo build
       Compiling own v0.1.0 (/data2/rust/closure3)
        Finished dev [unoptimized + debuginfo] target(s) in 0.32s
    [root@bogon closure3]# cat src/main.rs 
    #![allow(unused)]
    fn main() {
                      let mut num = 5;
                      let plus_num = || num + 1;
                      let num2 = &mut num;
    }
    [root@bogon closure3]# 
    [root@bogon closure3]# cargo build
       Compiling own v0.1.0 (/data2/rust/closure3)
        Finished dev [unoptimized + debuginfo] target(s) in 0.32s
    [root@bogon closure3]# cat src/main.rs 
    #![allow(unused)]
    fn main() {
                      //let mut num = 5;
                      //let plus_num = || num + 1;
                      //let num2 = &mut num;
                      let num = 5;
                      let plus_num = || num + 1;
                      let num2 = #
    }
    [root@bogon closure3]# 
    [root@bogon closure3]# cargo build
       Compiling own v0.1.0 (/data2/rust/closure3)
        Finished dev [unoptimized + debuginfo] target(s) in 0.33s
    [root@bogon closure3]# cat src/main.rs 
    #![allow(unused)]
    fn main() {
                      //let mut num = 5;
                      //let plus_num = || num + 1;
                      //let num2 = &mut num;
                      let num = 5;
                      let plus_num = || num + 1;
                      let num2 = num;
    }
    [root@bogon closure3]# 

    原理

    有些语言中没有 closure 和普通函数的区分,但 Rust 有。对 Rust 来说普通函数就是一段代码。而 closure 和 C++ 类似:每个 closure 会创建一个匿名的struct,编译器会在当前上下文捕获 closure 代码中的外部变量然后塞进这个结构体里面。

    这件事非常重要,请默念三遍一个 closure 就是一个捕获了当前上下文变量的结构体(外加一段代码,这不重要)。

    这解释了为什么 Rust 中两个参数和返回值一样的 closure 不被视作同一类型[1],因为它们背后的匿名结构体不同,有着不同的大小、字段和 lifetime。

    let m = 1.0;
    let c = 2.0;
    
    let line = |x| m*x + c;
    
    // 等价于
    
    struct SomeUnknownType<'a> {
        m: &'a f64,
        c: &'a f64
    }
    
    impl<'a> SomeUnknownType<'a> {
        fn call(&self, x: f64) -> f64 {
            self.m * x + self.c
        }
    }

    例子来源于 Why Rust Closures are (Somewhat) Hard

    这也是 closure 难用的根源:

    1. Rust 中结构体的可变性以及 liftime 本身就很烦人。
    2. Closure 的规则都是隐式的:closure 捕获值的方式及所生成的closure的类型都是按照隐式的规则决定的。
    3. Closure 一直会捕获整个复合类型,如 structtuple 和 enum 。而不只是单个字段[2]

    对于 (3),Rust 团队已经接受了一个提案,旨在改进不相交字段的捕获规则。(当前看起来没多少进展)

    为什么

    对于 (1) 和 (2) 是语言设计思路所带来的结果,为什么会这样呢?

    因为 closure 很好用,但是我们不想付出运行时代价。所有语言都有类似的东西,但是它们把 closure 捕获的结构丢到堆上以保证所有 closure 类型大小一样,且借助了 GC 管理资源。

    Rust选择「零额外开销」(Zero Overhead)所以必须用这种方式来实现 closure。使用高级抽象的同时保持了性能无损。比如说我们能用很函数式的方法处理迭代器,但最后生成的汇编和手写循环没什么区别。

    并且Rust提供了Box<Fn() -> T>Rc让你可以手动做到别的语言自动做到的事情。你需要显式使用这些设施,因为这代表额外的开销。

    而选择隐式的捕获规则是因为closure被设计为在某个特定上下文内以短小、简洁而频繁的方式书写[3]。因此采用了这种隐式且最保守的捕获方式。代价就是容易让人摸不着头脑。虽说利大于弊,但的确是一个缺点(参见下一节的引用部分)。

    规则

    捕获规则最简单的情形是 move || {...} 它会尝试获取closure中用到的值的ownership,如果值是 Copy 的则 copy 一个。

    而默认的捕获方式是:

    1. 如果可以,则尽量用 & 借用
    2. 否则,如果可以,则总是 &mut 借用
    3. 最后,无计可施必须要 ownership 的话,才会 move

    捕获之后,根据你在 closure 代码中如何使用捕获到的值,编译器会为 closure 实现函数 traits。最后实现了哪些 traits 和捕获的方式(有没有加move)或者捕获到了哪些变量是无关的。

    • 所有函数都至少能调用一次,所以都会实现FnOnce
      • 另外,对于那些不会移走匿名结构体中变量的 closure 实现 FnMut
        • 并且,对于那些不会修改匿名结构体中变量的 closure 实现 Fn

    FnOnceFnMut 和 Fn,下图中可以看出这三者是包含的关系。

    (Google Docs)

    其中FnMutFn能调用多次。FnMut调用时需要对自己匿名结构体的&mut self引用。调用Fn只需要&self引用就足够了。

    以下内容可以跳过。
    即使是面临必须要 ownership 的情况,如果值可以 Copy,编译器依然会避免 move,而是用 & 的方式借用值,之后在需要的时候 *。相关文章是《Rust 闭包环境捕获行为与 Copy trait》。
    我们都认为是 bug,直到语言团队成员回复说这是预料中的行为。之后我注意到这是规则1较为反直觉的特例。

    实践

    现在来写下不同类型的 closure。然后去看编译器产出的 MIR。

    MIR 是中级中间表示(简称中二表示)详细可以看官方博客的这篇文章。我们关注的只是少部分内容,大部分看不懂也没关系。

    总而言之,MIR 告诉我们「代码究竟会变成什么样」但又保留了类型信息,不像汇编那样面目全非。

    FnOnce

    Closure 中必须移走某个变量的 ownership,这种 closure 需要 self 来执行,所以只能 FnOnce

    Playground (点右上角 “RUN” 按钮旁的「…」按钮,再点 “MIR” 看结果。)

    fn main() {
        let homu = Homura;
        let get_homu = || homu;
        get_homu();
    }

    调用时的 MIR

    let mut _4: [closure@src/main.rs:9:20: 9:27 homu:Homura];
    let mut _5: ();
    _3 = const std::ops::FnOnce::call_once(move _4, move _5) -> bb1;

    可以看到它是以 FnOnce 方式调用的。

    _4 作为第一个参数传进去,它的类型 [closure@src/main.rs:10:20: 10:27 homu:Homura] 就是本文一直在叨念的匿名结构体了。其中 home:Homura 则是这个结构体捕获的变量和她的类型。

    _5: () 代表着无参数。

    Closure 代码所编译成的普通函数:

    fn main::(_1: [closure@src/main.rs:9:20: 9:27 homu:Homura]) -> Homura {
        let mut _0: Homura;                  // return place
    
        bb0: {                              
            _0 = move (_1.0: Homura);        // bb0[0]: scope 0 at src/main.rs:9:23: 9:27
            return;                          // bb0[1]: scope 0 at src/main.rs:9:27: 9:27
        }
    }

    注意这里 _1 的类型:[closure@src/main.rs:9:20: 9:27 homu:Homura] 前没有 & 或者 &mut,代表这个调用后会消耗掉匿名结构体。

    _0 = move (_1.0: Homura); 可以看见内部移走了 homu

    FnMut

    在 closure 中修改某个可变的引用[4],但无需移走任何捕获到的值。这种 closure 必须请求一个&mut,所以有FnMut

    Playground

    调用时:

    let mut _6: &mut [closure@src/main.rs:9:25: 9:41 madoka:&mut std::option::Option<Madoka>];
    let mut _7: ();
    _5 = const std::ops::FnMut::call_mut(move _6, move _7) -> bb1;

    Closure 所生成的函数体:

    fn main::(_1: &mut [closure@src/main.rs:9:25: 9:41 madoka:&mut std::option::Option<Madoka>]) -> () {
        // ...
    }

    可以看到 _1 变成一个 &mut 引用了。能多次调用而不会消耗匿名结构体。

    被捕获的值变成了 madoka:&mut std::option::Option<Madoka> 。于是在这个 closure 销毁之前别人都不能访问 madoka 了。

    Fn

    在 closure 中只会读取外部的值,只需要 &self 就能执行,当然全部三种都实现了。

    fn main() {
        let homu = Homura;
        let mado = Madoka;
        let marry = || (&homu, &mado);
        marry();
    }

    Playground

    调用时:

    let mut _7: &[closure@src/main.rs:10:17: 10:34 homu:&Homura, mado:&Madoka];
    let mut _8: ();
    _6 = const std::ops::Fn::call(move _7, move _8) -> bb1;

    是用 Fn 的方式调用的。

    Closure 生成的函数体:

    fn main::(_1: &[closure@src/main.rs:10:17: 10:34 homu:&Homura, mado:&Madoka]) -> (&Homura, &Madoka) {
        // ...
    }

    如果 closure 根本不捕获任何东西,则匿名结构体是 Zero Sized Types,在运行时不会被创建。这类 closure 等价于普通函数,自然也实现了全部三种。代码略。

    实现哪些 traits 和捕获到的值无关

    就算用 move 强制捕获变量的所有权,只要不移走它而仅仅是修改或读取它。这种情况依然会实现 FnMut 或 FnPlayground

    fn main() {
        let homu = Homura;
        let mado = Madoka;
        let marry = move || {
            (&homu, &mado);
        };
        marry();
    }

    这种代码,用了 move 所以会捕获 homu 和 mado 的所有权,但是MIR可以看到是通过 Fn::call 调用的:

    let mut _5: &[closure@src/main.rs:10:17: 12:6 homu:Homura, mado:Madoka];
    let mut _6: ();
    _4 = const std::ops::Fn::call(move _5, move _6) -> bb1;

    看看closure所生成的函数体吧:

    fn main::(_1: &[closure@src/main.rs:10:17: 12:6 homu:Homura, mado:Madoka]) -> () {
        let mut _0: ();                      // return place
        let mut _2: (&Homura, &Madoka);
        let mut _3: &Homura;
        let mut _4: &Madoka;
    
        bb0: {                              
            // ...
            _3 = &((*_1).0: Homura);
            _4 = &((*_1).1: Madoka);
            (_2.0: &Homura) = move _3;
            (_2.1: &Madoka) = move _4;
            // ...
            return;
        }
    }

    不同于前一个没有加 move 的例子。homu:Homura 和 mado:Madoka 前没有 &,代表匿名结构体捕获了这两个变量的所有权。

    然而捕获了那些变量的匿名结构体本身又是以 _1: &[closure...] 的方式传入的。因为函数体内根本不会移走 homu 或者 mado

    如果修改这份代码在 closure 过程内修改 mado 的话会变成什么样呢?留作习题。

    语法

    Closure看上去是这样的:

        let plus_one = |x: i32| x + 1;
        assert_eq!(2, plus_one(1));

    首先创建一个绑定plus_one,然后将它分配给一个closure,body是一个expression,注意{ } 也是一个expression。

    它也可以被写成这样:

        let plus_two = |x| {
            let mut result: i32 = x;
        
            result += 1;
            result += 1;
        
            result
        };
        assert_eq!(4, plus_two(2));
        

    和常规的函数定义相比,区别就是closure没有使用关键词 fn ,区分一下:

    fn  plus_one_v1   (x: i32) -> i32 { x + 1 }
    let plus_one_v2 = |x: i32| -> i32 { x + 1 };
    let plus_one_v3 = |x: i32|          x + 1  ;
    

    值得注意的是在closure中参数和返回值的类型都是可以省略的,下面这种形式也是可以的:

    let plus_one = |x| x + 1;

    闭包和它的环境

    一个小例子:

        let num = 5;
        let plus_num = |x: i32| x + num;
        
        assert_eq!(10, plus_num(5));
        

    也就是说,plus_num引用了一个在它作用于中的变量num,具体地说这是一个borrow,它满足所有权系统的要求,来看一个错误的例子:

    let mut num = 5;
    let plus_num = |x: i32| x + num;
    
    let y = &mut num;
    
    error: cannot borrow `num` as mutable because it is also borrowed as immutable
        let y = &mut num;
                     ^~~

    在上面的代码中,plus_num已经对num做了不可变引用,而在plus_one的作用域内,又发生了一次可变引用,所以就违反了所有权系统中的如下规则:

    如果对一个绑定进行了不可变引用,那么在该引用未超出作用域之前,不可以再进行可变引用,反之也是一样。

    对代码做出如下修改即可:

        let mut num = 5;
        {
            let plus_num = |x: i32| x + num;
        
        } // plus_num goes out of scope, borrow of num ends
        
        let y = &mut num;
    

    再看一个例子:

        let nums = vec![1, 2, 3];
        let takes_nums = || nums;
        println!("{:?}", nums);
        

    有问题吗?
    有,而且是大问题,编译器的报错如下:

    
    closure.rs:8:19: 8:23 error: use of moved value: `nums` [E0382]
    closure.rs:8    println!("{:?}", nums);
    

    从错误中可以看出来,在最后一个输出语句中,nums已经没有对资源 vec![1, 2, 3] 的 所有权了,该资源的所有权已经被move到了closure中去了。

    那么问题来了:

    为什么在前面的例子中closure是borrow,而到了这里就变成了move了呢?
    

    我们从头梳理一遍:

        let mut num = 5;
        let plus_num = || num + 1;
        let num2 = &mut num;
        
    Error:
    closure.rs:5:21: 5:24 error: cannot borrow `num` as mutable because it is also borrowed as immutable
    closure.rs:5     let num2 = &mut num;
    

    说明在closure中发生了immutable borrow,这样才会和下面的&mut冲突,现在我们来做一个改动:

        let plus_num = || num + 1; 
        // 改成如下语句
        let mut plue_num = || num += 1;

    再编译一次:

    Error:
    closure.rs:4:17: 4:20 error: cannot borrow `num` as mutable more than once at a time
    closure.rs:4 let num2 = &mut num;
    

    可以发现,在closure中发生了mutable borrow,为什么会这样呢?

    在closure无非就是这3种情况:

    • by reference: &T

    • by mutable reference: &mut T

    • by value: T

      至于是这3个中的哪一个,取决于你closure内部怎么用,然后编译器自动推断绑定的类型是Fn() FnMut() 还是FnOnce()

        let plus_num = || num + 1;         // 这个只需要引用即可,所以plus_num类型为Fn()
        let mut plue_num = || num += 1;    // 这个则需要&mut T,所以plus_num类型为FnMut()
        // 这是手册里的一个例子
        // 这是一个没有实现Copy trait的类型
        let movable = Box::new(3);
        // `drop` 需要类型T,所以closure环境就需要 by value T.,所以consume类型为FnOnce()
        let consume = || {
            drop(movable);    // 这里发生了move
        };
        // 所以这个consume只能执行一次
        consume();
    

    有一点要注意的是:
    在前面的例子应该分成两类:

    1. let a= 100i32;

    2. let a = vec![1,2,3];

    区别就是i32类型实现了copy trait,而vector没有!!!

    参考:http://rustbyexample.com/fn/closures/capture.html

    Move closure

    使用move关键字,强制closure获得所有权,但下面的例子得注意一下:

        let num = 5;
        let owns_num = move |x: i32| x + num;

    尽管这里使用move,变量遵循move语义,但是,在这里5实现了Copy,所以owns_own获得的是 5 的拷贝的所有权,有什么区别呢?
    来看看这段代码:

        let mut num = 5;
        {
            let mut add_num = |x: i32| num += x;
            add_num(5);
        }
        assert_eq!(10, num);    

    这段代码得到的是我们想要的结果,但是如果我们加上move关键字呢?上面的代码就会报错,因为num的值仍是 5 ,并没有发生改变,

    为什么呢?
    上面说到了,move强制闭包环境获得所有权,但是 5 实现了Copy,所以闭包获得的是其拷贝的所有权,同理闭包中修改的也是 5 的拷贝。
    

    总结

    在Rust中闭包的概念并不好理解,因为牵扯到了太多所有权的概念,可以先把所有权弄懂了,闭包也就好理解了。

  • 相关阅读:
    Gridview利用DataFormatString属性设置数据格式
    PowerDesigner15 逆向工程
    JS实现动态显示当前时间(12/24小时制)(转载Mr.Think)
    如何判断Javascript对象是否存在
    PHP学习(一):判断数组中的值是否包含某字符
    JavaScript显示剩余时间
    Hibernate:a different object with the same identifier value was already associated wit异常解决
    excel含文字求和
    excel去掉最高分
    excel保护
  • 原文地址:https://www.cnblogs.com/dream397/p/14190578.html
Copyright © 2011-2022 走看看