Rust 中的闭包及捕获上下文环境变量使用和更改及闭包的引用


发布者 ourjs  发布时间 1657163803907
关键字 Rust 

Rust 中的闭包及捕获上下文环境变量使用和更改及闭包的引用

闭包定义

在Rust中闭包是一个可以捕获其环境的匿名函数。在这个定义中有两点需要强调:

  • 闭包可以想象成一个函数;
  • 与函数相反,它可以捕获其环境(捕获环境意味着在闭包中您可以使用在闭包主体之外定义但在其范围内可访问的变量)。

简单的示例:

fn main() {
    let count = 0;
    let print_count_closure = || println!("Count value: {}", count);
    print_count_closure();
}

闭包表达式 || println!("Count value: {}", count)生成一个闭包值,该闭包值存储在print_count_closure具有唯一匿名类型的变量中。此闭包捕获变量 count。我们通常在闭包print_count_closure这个术语下所理解的就是示例中的变量中存储的内容。

闭包传参

可以将参数传递给闭包。为此,只需在 bar 内添加参数名称||。例如,在下面的示例中,闭包接受两个参数name和age.

fn main() {
    let intro = String::from("Participant age");
    let print_user_age = |name, age| println!("{}\n\t{}: {}", intro, name, age);

    for (name, age) in [
        (String::from("Alice"), 5),
        (String::from("Bob"), 7),
        (String::from("Mallory"), 20),
    ]
    .iter()
    {
        print_user_age(name, age);
    }

    //print_user_age(String::from("Eve"), "infinity"); //error: mismatched types
}

注意,我们没有义务指定参数的类型,因为Rust 会考虑作为参数传递给闭包的值的类型来推断它们。但是,如果使用不同类型的参数调用闭包,则会产生编译时错误(请参阅闭包的注释调用)。

可变捕捉模式

闭包和函数之间的主要区别在于前者可以捕获变量,这意味着它们可以在计算中使用在同一范围内可访问的变量的值。与从参数获取输入的函数相反,其访问模式是显式定义的(不可变借用、可变借用或移动),闭包隐式捕获变量。

fn main() {
    let immut_val = String::from("immut");
    let fn_closure = || {
        println!("Len: {}", immut_val.len());
    };

    println!("Value: {}", immut_val); //ok
    fn_closure();                     //ok

    // cannot borrow mutably because it is already borrowed immutably
    // immut_val.push_str("-push");   
    // fn_closure();
}

因此,由于值没有被修改,闭包通过不可变的借用来捕获变量。这意味着我们可以对变量进行其他不可变引用.

可变借用捕获

在下面的示例中,我们创建mut_val类型String为 value的可变变量"mut"。然后,我们创建一个闭包,将子字符串附加到我们的"-new"字符串中。

fn main() {
    let mut mut_val = String::from("mut");
    let mut fnmut_closure = || {
        mut_val.push_str("-new");
    };
}

您可以注意到与上一个示例的几个不同之处。第一个很清楚:在 Rust 中,如果您更改变量的值,则需要将其声明为可变的。但是,第二个并不那么明显:如果闭包修改了 environment ,则必须将闭包变量声明为可变的(添加mut)。

所以当值被修改时,闭包通过可变借用来捕获变量(有时,可变借用也称为唯一可变借用)。因此,我们不能同时对变量进行其他引用:

fn main() {
    let mut mut_val = String::from("mut");
    let mut fnmut_closure = || {
        mut_val.push_str("-new");
    };

    // cannot borrow immutable because the variable is borrowed as mutable
    // println!("{}", mut_val);
    
    // cannot borrow mutably second time
    // mut_val.push_str("another_string");
    
    fnmut_closure();

    // ok because closure is already dropped 
    println!("{}", mut_val); 
}

移动捕获

在下面的示例中,创建了一个String名为的变量mov_val。然后,在闭包中我们使用hack来移动一个值:我们将变量的值分配mov_val给一个新的变量moved_value。

fn main() {
    let mov_val = String::from("value");
    let fnonce_closure = || {
        let moved_value = mov_val;
    };
}

我们知道,在 Rust 中,将非复制类型变量分配给新变量会移动新变量的值(更改值的所有者)。因此,在闭包体中,我们移动了闭包变量的所有权。因此,闭包通过 move 捕获变量。所以当变量被移动时,我们不能在其他任何地方使用它:

fn main() {
    let mov_val = String::from("value");
    let fnonce_closure = || {
        let moved_value = mov_val;
    };
    
    fnonce_closure(); // ok
    
    // cannot print it because it is captured in the closure
    // borrow of moved value
    // println!("{}", mov_val);
    // cannot call closure the second time
    // fnonce_closure();
}

通过唯一不可变借入捕获

图中没有考虑一种特殊情况,通常也不会在书籍和文章中提及:通过唯一不可变借用捕获。我在阅读关于闭包的 Rust 参考章节时发现了这种情况。当您捕获对可变变量的不可变引用并使用它来修改引用的值时,就会出现这种情况。例如,让我们考虑以下示例:

fn main() {
    let mut s = String::from("hello");
    let x = &mut s;
    
    let mut mut_closure = || {
        (*x).push_str(" world");
    };
}

在这里,闭包捕获了不可变变量x,即对可变变量的引用String。闭包不会修改引用值,因此闭包应该x通过不可变借用来捕获。因此,我们应该能够同时对该变量进行其他引用。但是,这是不正确的,例如,在以下编译错误的示例中:

fn main() {
    let mut s = String::from("hello");
    let x = &mut s;
    
    let mut mut_closure = || {
        (*x).push_str(" world");
    };

    // cannot borrow `x` as immutable because previous closure requires unique access
    println!("{:?}", x); // error happens here
    mut_closure();
}

原因是在这种情况下,变量被唯一的不可变借用捕获。据我了解,这种情况比较少见,编译器很擅长说是什么问题,所以这种情况通常会省略(图中也没有单独挑出来)。但是,对于该主题的完整介绍,我将在本节中讨论它。

联合捕获

目前,任何在闭包中命名的变量都将被完全捕获。如果您捕获复杂类型的变量,例如结构,这会带来一些不便。让我们考虑以下人工示例:

struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let mut alice_wonderland = Person {
        first_name: String::from("Alice"),
        last_name: String::from("Wonder"),
    };
    let print_first_name = || println!("First name: {}", alice_wonderland.first_name);
    alice_wonderland.last_name.push_str("land");
    print_first_name();
}

在这个例子中,我们创建了一个名为的结构体Person,它由两个字段组成:first_name和second_name。然后,在main函数中,我们创建这个结构的一个实例,alice_wonderland. 我们定义一个打印名字的闭包,然后我们修改存储在last_name实例字段中的值。虽然闭包和修改语句处理Person实例的不同字段,但这段代码会产生错误:

error[E0502]: cannot borrow `alice_wonderland.last_name` as mutable because it is also borrowed as immutable
  --> src/bin/disjoin_fields.rs:12:5
   |
11 |     let print_first_name = || println!("First name: {}", alice_wonderland.first_name);
   |                            --                            ---------------- first borrow occurs due to use of `alice_wonderland` in closure
   |                            |
   |                            immutable borrow occurs here
12 |     alice_wonderland.last_name.push_str("land");
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
13 |     print_first_name();
   |     ---------------- immutable borrow later used here

error: aborting due to previous error

出现此错误是因为闭包尽管只处理了结构的一个字段,但仍不可变地借用了整个结构实例,因此,您不能同时修改结构的另一个字段。

这与当前的借用和移动规则产生了不一致。有一个关于单独捕获不相交字段的RFC 提案。希望很快它将在 Rust 中实现(很可能在该语言的新版本中)。

2021 年 15 月 11 日更新:使用Rust 2021 版(Rust 1.56),闭包开始捕获单个结构字段。

调用闭包

如果您仅通过直接调用闭包(通过在参数值中添加括号)来使用闭包,这不会给您带来很多好处。这类似于闭包体的简单内联,而不是相应的调用。

当您将闭包作为参数传递给调用它们的其他函数(通常称为高阶函数)时,闭包的真正威力就会显现出来。例如,闭包可以用作回调或迭代器适配器中元素的处理器。在 Rust 中,为了定义一个函数,我们需要知道函数参数的类型或指定它们的 trait bound。

众所周知,闭包变量有一个唯一的、无法写出的匿名类型。为了举例说明这一点,让我们考虑以下将假类型分配给我们的闭包变量的示例:

fn main() {
    let count = 0;
    let print_count_closure: i32 = || println!("Count value: {}", count);
    print_count_closure();
}

如果我们尝试构建这个程序,编译器会产生以下错误:

error[E0308]: mismatched types
 --> src/bin/simple_closure_call.rs:3:36
  |
3 |     let print_count_closure: i32 = || println!("Count value: {}", count);
  |                              ---   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found closure
  |                              |
  |                              expected due to this
  |
  = note: expected type `i32`
          found closure `[closure@src/bin/simple_closure_call.rs:3:36: 3:73]`

如您所见,编译器期望[closure@src/bin/simple_closure_call.rs:3:36: 3:73]我们的print_count_closure变量有一个奇怪的类型。但是,即使您将此类型添加到示例中,编译器仍然会报错。

同样,我们不能在函数定义中指定闭包参数的类型。因此,定义函数应该接受闭包作为参数的唯一方法是通过特征边界。Rust 提供了三种不同的 trait Fn、FnMut和FnOnce,它们可以用作闭包参数的 trait bound。每个闭包都实现了这三个特征之一,而自动实现的特征取决于闭包如何捕获其环境。

我们在图 1 中的图表解释了捕获模式之间的联系以及自动为闭包实现的特征。用文字来说,这可以用以下一组规则来表达:

非捕获闭包(不从其环境中捕获变量的闭包)或仅通过不可变借用捕获变量的闭包会自动实现Fntrait。 如果所有变量都被可变和不可变借用捕获,那么闭包会自动实现FnMuttrait。 如果 move 至少捕获了一个变量,则闭包会自动实现FnOncetrait。 此外,在图表上,您可以看到:闭包特征之间的列 ( ) 符号。这些列显示了相应特征之间的超特征关系。例如,FnOnce是 的超特征FnMut。反过来,FnMut可以将其视为特质的超Fn特质。这意味着如果你有一个FnOnce对参数施加特征绑定的函数,你也可以传递给它FnMut和Fn闭包。实际上,如果您的函数需要一个只能调用一次的闭包(FnOnce特征绑定),那么很明显,您可以传递一个不修改环境(Fn)或修改环境()的闭包,FnMut因为它们将被调用只有一次。

让我们考虑以下示例。让我们定义函数call_fn,call_fn_mut它call_fn_once接受一个具有特征绑定的泛型参数,允许函数接受Fn,FnMut并FnOnce相应地闭包并调用它们。请注意,call_fn_mut强制闭包参数是可变的。

fn call_fn<F>(f: F)
where
    F: Fn(),
{
    f();
}

fn call_fn_mut<F>(mut f: F)
where
    F: FnMut(),
{
    f();
}

fn call_fn_once<F>(f: F)
where
    F: FnOnce(),
{
    f();
}
...

现在,让我们尝试使用上一节中定义的闭包作为这些函数的参数:

fn main() {
    let immut_val = String::from("immut");
    let fn_closure = || {
        println!("Len: {}", immut_val.len());
    };

    let mut mut_val = String::from("mut");
    let mut fnmut_closure = || {
        mut_val.push_str("-new");
    };

    let value = String::from("value");
    let fnonce_closure = || {
        let moved_value = value;
    };

    call_fn(fn_closure);
    call_fn_mut(fn_closure);
    call_fn_once(fn_closure);

    // call_fn(fnmut_closure); //error: fnmut_closure implements `FnMut`, not `Fn`
    call_fn_mut(fnmut_closure);
    call_fn_once(fnmut_closure);

    // call_fn(fnonce_closure); //error: fnonce_closure implements `FnOnce`, not `Fn`
    // call_fn_mut(fnonce_closure); //error: fnonce_closure implements `FnOnce`, not `FnMut`
    call_fn_once(fnonce_closure);
}

正如你所看到的,闭包fn_closure自动实现了这个Fn特征,因为它只以不可变的方式借用变量,可以作为参数传递给这三个函数中的任何一个。这意味着如果闭包实现了Fntrait,它也会自动实现FnMut和FnOncetrait。

从错误解释中,我们了解到fnmut_closure闭包没有实现Fntrait。同样,fnonce_closure闭包也没有实现FnandFnMut特征。因此,它们不能作为参数传递给相应的函数。

对闭包的引用

需要强调的是:

如果 typeF实现了Fntrait,那么&F也实现了Fntrait。 如果 typeF实现了FnMuttrait,那么&mut F也实现了FnMuttrait。 这意味着在我们的示例中,我们可以将引用传递给它们,而不是传递给实现Fn和特征的高阶函数闭包:FnMut

fn main() {
    let immut_val = String::from("immut");
    let fn_closure = || {
        println!("Len: {}", immut_val.len());
    };

    let mut mut_val = String::from("mut");
    let mut fnmut_closure = || {
        mut_val.push_str("-new");
    };
    call_fn(&fn_closure);
    call_fn_mut(&fn_closure);
    call_fn_once(&fn_closure);

    // call_fn(&mut fnmut_closure); //error fnmut_closure implements `FnMut`, not `Fn`
    call_fn_mut(&mut fnmut_closure);
    call_fn_once(&mut fnmut_closure);
}

请注意,这种类比不适用于实现该FnOnce特征的闭包。FnOnce闭包只能按值使用 。 移动关键字 让我们考虑以下示例。在这里,我们定义了一个调用的函数,该函数get_print_closure返回一个闭包,该闭包打印name在同一函数中定义的变量的值。

fn main() {
    let closure = get_print_closure();
    closure();
}

fn get_print_closure() -> impl Fn() {
    let name = String::from("User");
    || { //error
        println!("Hello {}!", name);
    }
}

如果我们分析闭包的主体,我们就会明白name变量的值是通过不可变引用捕获的。在函数以闭包作为返回值返回后,该变量name将超出范围并应被删除。然而,在这种情况下,我们会得到一个不正确的内存状态,一个悬空指针。显然,这段代码导致了 Rust 中的错误:

error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
 --> src/bin/move_keyword.rs:8:5
  |
8 |     || {
  |     ^^ may outlive borrowed value `name`
9 |         println!("Hello {}!", name);
  |                               ---- `name` is borrowed here
  |
note: closure is returned here
 --> src/bin/move_keyword.rs:6:27
  |
6 | fn get_print_closure() -> impl Fn() {
  |                          

幸运的是,错误告诉我们如何纠正这个问题:我们只需要move在闭包的第一个 bar 之前添加关键字:

fn get_print_closure() -> impl Fn() {
    let name = String::from("User");
    move || { //error
        println!("Hello {}!", name);
    }
}

如果我们添加这个神奇的关键字会发生什么?在这种情况下,name变量将被移动到闭包内(闭包将编码name变量的所有者)并且不会产生错误。

除了在返回闭包的表达式中使用之外,该move关键字还经常在我们创建新线程并在其中执行闭包时使用。例如,除非我们取消注释move关键字,否则以下程序将无法编译:

use std::thread;

fn main() {
    let name = String:: from("Alice");
    let print_closure = /*move*/ || println!("Name: {}", name);
    let handler = thread::spawn(print_closure);
    handler.join().expect("Error happened!");
}

结论

这就是现在的全部。希望这对您有用。由于我是 Rust 新手,我花了很多时间来理解这个主题并写这篇文章,但是我并不后悔,因为我达到了这个理解水平。如果您发现任何错误或对我应该扩展哪些子主题有建议,如果您指出我将不胜感激,以便我改进文章。

参考原文: https://zhauniarovich.com/post/2020/2020-12-closures-in-rust/