Rust closures – Fn/FnMut/FnOnce traits, capturing and passing parameters

Rust closure: is a type of function that can capture variables in the surrounding scope

|Parameters| {Function body}

  • Parameter and return value types can be deduced without displaying annotations.
  • Type uniqueness, cannot be changed once determined
  • When the function body is a single expression, {} can be omitted

Article directory

    • introduction
    • 1 Category Fn / FnMut / FnOnce
    • 2 Keywords move
    • 3 Closures passed as parameters

Introduction

The biggest difference between closures and general functions is that they can capture variables in the surrounding scope (not necessarily the same scope as the current one, but also in the upper level); of course, you can also choose not to capture anything.

let a = 0;

//General function
// fn f1 () -> i32 {a} // Error: Dynamic environment variables cannot be captured in fn

// Closure
let f2 = || println("{}", a); // Closure capture & amp;a
let f3 = |a: i32|{<!-- -->}; // The closure captures nothing, a is just an ordinary parameter

The capture mentioned here should not be thought of as simply passing parameters like a function. It can be understood that closure is also a kind of syntactic sugar. The operations behind it are much more complicated. For details, please refer to the relevant information at the end of the article [1]

// As an example, the following closure is defined and called
let message = "Hello World!".to_string();
let print_me = || println!("{}", message);

print_me();

The actual operation is as follows:

#[derive(Clone, Copy)]
struct __closure_1__<'a> {<!-- --> // note: lifetime parameter
    message: & amp;'a String, // note: & amp;String, the so-called capture reference will be mentioned below
}

impl<'a> Fn<()> for __closure_1__<'a> {<!-- -->
    // type Output = ();
    
    fn call( & amp;self, (): ()) -> () {<!-- -->
        println!("{}", *self.message)
    }
}

let message = "Hello World!".to_string();
let print_me = __closure_1__ {<!-- --> message: & amp;message };


Fn::call( & amp;print_me, ());

1 Category Fn / FnMut / FnOnce

According to the operations performed on the captured variables, there are three types of traits implemented by closures in Rust.
Attention! The causal relationship here is that the operation of capturing variables determines the form of closure implementation

  • Fn: Can be called repeatedly without changing state; captures an immutable reference to a variable (shared reference) or captures nothing at all
  • FnMut: can change the state and can be called repeatedly; captures the mutable reference of the variable (mutable reference)
  • FnOnce: Can only be called once, there is a captured variable ownership transfer that is consumed
// The closure impl trait compiler will automatically deduce it based on the capture operation, and the comments are easy to read.
let a = 0;
// impl Fn()
let f1 = || println("{}", a); // Capture & amp;a
f1();
f1();

let mut b = 0;
// impl FnMut()
let mut f2 = || b + =1; // Capture & amp;mut b; You may have questions why you don’t need to dereference *b + =1, please refer to related information [1]
f2();
f2();

let c = "".to_string();
// impl FnOnce()
let f3 = || std::mem::drop(c);
f3();
//f3(); // Error, f3 can only be called once, c ownership has been transferred and consumed

2 Keyword move

moveConverts any variable captured by reference or mutable reference to a variable captured by value
Attention! The traits a closure implements are determined by the operations performed on the value, not how the value is captured; this means that even if a value is captured in the closure and ownership is transferred, it may be Fn or FnMut [2]

(1) Objects that implement the Copy trait, value copying occurs when moving

let a = 0;
// impl Fn()
let f1 = move || println("{}", a); // Convert the captured immutable reference into a value copy and pass it to the closure

let mut b = 0;
// impl FnMut()
let mut f2 = move || b + = 1;
f2();
f2();
println("{}", b); // Because the value is copied in the closure, it is still 0

(2) For objects that do not implement the Copy trait, ownership transfer occurs when moving

let a = "".to_string();
// impl Fn()
let f1 = move || println!("{}", a); // The ownership of the value corresponding to variable a in the environment is transferred to closure a
// Because no consumption is generated, the type derivation is still Fn, and f1 can be called repeatedly
f1();
f1();
// println("{}", a); // Error reported, a whose value has been moved is used

let mut b = "".to_string();
// impl FnMut()
let mut f2 = move || {<!-- -->
b + = "x";
println("{}", b);
};
f2(); // x
f2(); //xx
// println("{}", b); // Error reported, b whose value has been moved is used

let c = "".to_string();
// impl FnOnce()
let f3 = move || {<!-- -->
println("{}", c);
std::mem::drop(c); // Whether there is a move here is actually the same. The closure drop does not implement the value of Copy. The default capture is the environment variable whose ownership has been transferred.
};
f3();

(3) Some points that need attention

  • In a closure, if the environment variable is directly used as the return value, it will be returned in the form of a value [1]
// Implements Copy type data
let mut a = 0;
// impl FnMut() -> i32
let mut f1 = || {<!-- -->
a + = 1; // Capture a reference
a // Without ";", the return value of closure type derivation is i32
};
f1();
f1();
println!("{}", a); // 2

// Copy type data is not implemented
let mut b = "".to_string();
// impl FnOnce() -> String
let mut f2 = || {<!-- -->
b + = "x"; // Capture b of ownership transfer
b // There is no ";" to return the ownership transfer b; Because the ownership is transferred and passed (consumed) as the return value, it cannot be called repeatedly, so the type derivation is FnOnce
}
f2();
  • Some scenarios will trigger implicit moves for variables that do not implement Copy.
    (No relevant information found, so I can only rely on memory for now)
// std::mem::drop refer to the previous example

// path statement
let a = "".to_string();
// impl FnOnce()
let f1 = || {<!-- -->a;};

// operation statement
let b = "".to_string();
// impl FnOnce()
let f2 = || {<!-- -->b + "x";};

3 Closures are passed as parameters

Fn inherits from FnMut inherits from FnOnce

Conclusions can be drawn based on the inheritance relationship:

  • When the formal parameter type is Fn, only Fn can be passed
  • When the formal parameter type is FnMut, you can pass Fn, FnMut
  • When the formal parameter type is FnOnce, all three options are available:

Definition:

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() -> () {<!-- -->}

Call:

// 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( & amp;mut f2);

is_fn_once(f1);
is_fn_once( & amp;mut f2);
is_fn_once(f3);

Attention! ! ! is_fn_mut(f2) cannot be called here
The reason is that the closure itself, as Fn* type data, also needs to consider the implementation of its own Copy trait: Reference [3]

  • If no capture occurs, or a value copy is captured, or only an immutable reference (shared reference) is made, then the closure itself also implements the Copy trait;
// impl Fn(), uncaught
let fn_f1 = || {<!-- -->};
is_fn(fn_f1);
is_fn(fn_f1);

// impl FnMut(), capture value copy
let mut a = 0;
let mut fnmut_f2 = move || count1 + = 1;
is_fn_mut(fnmut_f2);
is_fn_mut(fnmut_f2);

// impl Fn(), capture immutable reference
let b = 0;
let fn_f3 = || println("", b);
is_fn(fn_f3);
is_fn(fn_f3);
  • If what is captured is a mutable reference (mutable reference), then the closure itself does not implement the Copy trait, and you need to pay attention to the possibility of ownership transfer.
fn is_fn_mut<F>(_: F) where F: FnMut() -> () {<!-- -->}

let mut count = 0;
// impl FnMut()
let mut f2 = || count + = 1;
is_fn_mut(f2); // It’s okay to call it only once, but at this time the ownership of f2 has already moved
//is_fn_mut(f2); // Error reported, f2 where move occurred was used

If you want to call multiple times, you need to pass & amp;mut f2; & amp;mut F also implements FnMut, so pass it here There is no problem with citation, refer to [4]

is_fn_mut( & amp;mut f2);
is_fn_mut( & amp;mut f2);

Relevant information:
[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 Other implementers
English https://doc.rust-lang.org/core/marker/trait.Copy.html
in https://rustwiki.org/zh-CN/std/marker/trait.Copy.html
[4] https://rustwiki.org/zh-CN/std/ops/trait.FnMut.html