Summary of Rust closure

  • Fn / FnMut / FnOnce

    The essence of closure is to implement the anonymous structure of one of the above three call trait s. Capturing environment variables refers to the way the anonymous structure treats environment variables. You can access environment variables in the anonymous structure only with immutale reference, modify environment variables through a mutable reference, or consume environment variables. First, in the case of single thread without the move keyword, when the code in the closure accesses the environment variable only through immutable reference, the type of the whole closure is Fn()+Copy. Why Copy is realized? The type of Copy automatically implemented in Rust is generally scalar, while structure and enum do not automatically implement Copy. To implement Copy, you must manually write #[derive(Copy, Clone)] and all fields can be copied. When the method of using environment variables inside a closure is immutable reference, this type of reference can be copied. It's convenient for us to write [clone #]. After adding move, the anonymous structure has the ownership of the captured variable, so the trait of Copy cannot be realized.

    Different closures have different structures, so the closure type is DST. When you need to use closures as parameters, you can use impl or trait object or generic.

    move affects whether the closure moves the environment variable into the anonymous structure during initialization.

    How to use environment variables in the closure body affects what kind of Trait the closure implements, that is, the type of closure itself.

    How to use variables determines what kind of trail closure implements. What does it mean?

    • Closures only use immutable references to access environment variables, and closures implement Fn
    • The closure body accesses environment variables with variable references, and the closure implements FnMut
    • If the closure method can only be called once, it implements FnOnce. (indicates that the closure consumes the captured variables).

    Rust will capture environment variables according to the closure body. The mode priority order of variables in the closure capture environment is: immutable borrowing, variable borrowing and ownership.

    fn main() {
        // In the closure body of the following code, it does not obtain the ownership of variables or transfer the ownership of variables,
        // So the type of this closure is FN () - > box < string >
        // After the closure captures s, the original variable can still be used
        let s = String::from("gef");
        let t = || {
            let s = &s;
            let g = Box::new((*s).clone());
            g
        };
        println!("{}", s);
    
        // There is only one difference between the closure body of this closure and the above, that is, the addition of move, which will forcibly take away the ownership of s, that is, affect the capture method.
        // ****Even if the * * * * closure appears to be a variable in the environment used to operate with the most common immutable reference, it doesn't seem to take away the ownership of the variable
        let s = String::from("gef");
        let t = move || {
            let s = &s;
            let g = Box::new((*s).clone());
            g
        };
        // Without comments, the compilation will fail, and s ownership has been taken away
        // println!("{}",s);
        // Even if move is used, the type of this closure is still FN () - > box < string >
        let fuck:Box<dyn Fn()->Box<String>>=Box::new(t);
    
        let s=String::from("gef");
        let t=||s;
    
        // This line of code cannot be compiled because the ownership of s has been moved to the closure, but if the closure body is & s, it can be compiled
        // println!("{}",s)
        // Whether move is used or not, the type of this closure is fnonce() - > string, because what kind of trail the closure implements only depends on how to use the captured variables
        // However, the code of the closure body affects how to capture variables to a certain extent. Rust will decide whether to transfer the ownership of variables to the closure body through derivation
        // For types that do not implement copy trail, if you do not add move, the ownership of environment variables will be taken away, depending on the use method in the closure body
        // **However, if move is added, no matter how the closure is written, the captured environment variable (copy trail is not implemented) will be taken away**
    }
    

    The initialization behavior of a closure will only be executed once, that is, at the closure definition, not at the closure execution. In this process, variables will be captured only once at the definition of closure, and no capture operation will be carried out at later execution.

    fn main(){
    		let s=String::from("111");
        let g=||s;
        g();
        let g=||s; // 🔺
        g(); // 🔺
    }
    

    Even if the environment variable is recovered, the closure will not be captured again. At the closure definition, the variable has been moved to the closure body, and then the variable is consumed in the first execution, and the variable capture action will not be carried out in the second execution, so the variable is gone. Therefore, the second execution of closure will report an error.

    It can be seen from here that if a closure can only be executed once, it must take the ownership of environment variables, so this closure must be of FnOnce type.

    In fact, FnOnce is a trail implemented by each closure, because each closure can be executed at least once, and then gradually specialize its implemented trail according to the way the closure uses variables.

    If the closure takes away the ownership of the environment variable and cannot be executed again, the type of this closure must be FnOnce

    However, the reason why the second execution cannot be performed may be the move keyword of the closure. However, the move keyword does not affect the type of closure. What kind of trail a closure implements still depends only on how variables are used inside the closure. For example, the following code can be compiled

    fn test(_t:impl FnOnce()){
    
    }
    
    fn main() {
        let s = String::from("abc");
        let f =   move || println!("{}",s);
        test(f);
    }
    // 
    

    Here is a wonderful example:

    fn main(){
        let mut a = 1;
        let mut inc = || {a+=1;a};
        println!("now a is {}", a);
    		inc();
    }
    
    let mut inc = || {a+=1;a};
      |                   --  - first borrow occurs due to use of `a` in closure
      |                   |
      |                   mutable borrow occurs here
    4 |     println!("now a is {}", a);
      |                             ^ immutable borrow occurs here
    5 |     inc();
      |     --- mutable borrow later used here
    

    Obviously, the borrowing behavior of closures has occurred at the time of initialization. Why does this happen? Doesn't it mean that closures are not executed during initialization? The reason is that the closure itself is an anonymous structure. During initialization, external variables will be captured into the structure in the corresponding way according to the code in the closure. Here, it is obviously mutable borrow; When the closure is executed for the time node actually executed to a+1, the essence of execution is that the anonymous structure operates its own member variables. Therefore, borrowing occurs during initialization, so this error is reported.

    fn main(){
        let mut s = String::from("test");
        let mut f = || {s.push('a');println!("{}", s)};
        f();
        f();
    }
    

    The reason why this closure can be executed many times is that the ownership of S is directly taken to the anonymous structure. Every time the closure is actually push ed, it is executed on s in the anonymous structure. This is also why when there is a modification operation in the closure, the closure itself must be modified with mut, because when the modification operation is performed, the closure itself must be mut.

    move keyword:

    This thing cannot be confused with FnOnce. In some cases, we need the closure itself (anonymous structure itself) to obtain the ownership of variables. Even if the code in the closure does not need the ownership of variables (consider passing closures between processes), we need to use the move keyword to force Rust to move all captured variables to the closure itself, so as to prolong the lifetime of the moved object.

  • Receive closures as parameters & & return closures (the method of receiving closures has one more trail bounds than returning closures)

    Since the impl method does not involve generics, some generic qualifiers cannot be used. More often, code that receives closures tends to use the trail bounds approach.

    • Receive closure

      You can use the Box dyn written in Rust book to receive, but there are runtime penalties. You can also use generic + trait bounds or impl trail

      dyn mode:

      fn fuck(f:Box<dyn Fn(i32)->i32>,arg:i32){
          println!("{}",f(arg))
      }
      
      fn main(){
          let f=|num:i32|num+1;
          fuck(Box::new(f), 1);
      }
      

      You can also use generic + trail bounds:

      fn fuck<F>(f:F,arg:i32)
      where
      		F:Fn(i32)->i32
      {
          println!("{}",f(arg))
      }
      
      fn main(){
          let f=|num|num+1;
          fuck(f, 1);
      }
      

      impl trait:

      fn fuck(f:impl Fn(i32)->i32,arg:i32){
          println!("{}",f(arg))
      }
      
      fn main(){
          let f=|num|num+1;
          fuck(f, 1);
      }
      
    • Return closure

      When you need to return a Trait, you can generally return the specific type that implements the Trait. However, a closure can only be expressed as a Trait and Trait is a kind of DST, so you can't directly return a closure: it doesn't have a specific type, it can only be expressed as a Triat. They can use the pointer of tradyn to complete a certain task, but the size of tradyn is different.

      dyn is written to clarify semantics. Otherwise, trail object and trail are too similar.

      fn main() {
          let f = return_a_closure();
          let t = f(1);
          assert_eq!(t, 2);
      }
      fn return_a_closure() -> Box<dyn Fn(i32) -> i32> {
          Box::new(|x: i32| x + 1)
      }
      

      The above paragraph is from the official tutorial of Rust, but now there are ways to return closures other than trail object. https://doc.rust-lang.org/reference/types/impl-trait.html#abstract-return-types

      The following is from Rust by Example:

      **// This function returns a closure, but the return value is limited by the type here. It not only writes and implements the trail of Fn,
      // It also details the parameter types received by the closure and the parameter types returned**
      fn returns_closure() -> impl Fn(i32) -> i32 {
          |x| x + 1 
      }
      
      fn main(){
          println!("{}",returns_closure()(1))
      }
      

      impl trait syntax from https://doc.rust-lang.org/std/keyword.impl.html Last paragraph:
      The other use of the impl keyword is in impl Trait syntax, which can be seen as a shorthand for "a concrete type that implements this trait". Its primary use is working with closures, which have type definitions generated at compile time that can't be simply typed out.

      T rust by example explains: impl trait provides ways to specify unnamed but concrete types that implement a specific trait It can appear in two sorts of places: argument position (where it can act as an anonymous type parameter to functions), and return position (where it can act as an abstract return type).

      fn thing_returning_closure() -> impl Fn(i32) -> bool {
          println!("here's a closure for you!");
          |x: i32| x % 3 == 0
      }
      

      a concrete type that implements this trait "can explain why the parameter list and return value list of the closure should be written clearly, because impl keyword refers to the concern type that implements this Trait. Since it is concern type, it is necessary to write all kinds of restrictions clearly.

      It can also be understood that the type of closure is unique, and the type that cannot be written can only be written through Fn trait.

      Impl trail can be written more simply than trail object. And because the Box is wrapped in the trail object, there is a runtime penalty.

  • Complete example

    fn receive1<T>(f: T)
    where
        T: Fn(i32) -> i32,
    {
        let num = 4;
        let num = f(num);
        println!("{}", num);
    }
    
    fn receive2(f: impl Fn(i32) -> i32) {
        let num = 10;
        let num = f(num);
        println!("{}", num);
    }
    
    fn receive3(f: Box<dyn Fn(i32) -> i32>) {
        let num = 15;
        let num = f(num);
        println!("{}", num);
    }
    
    fn return1() -> impl FnOnce(String) -> String {
        let greeting = String::from("hello budy, ");
        move |name: String| format!("{} {}", greeting, name)
    }
    
    fn return2() -> Box<dyn FnOnce(String) -> String> {
        let greeting = String::from("hello budy, ");
        Box::new(move |name: String| format!("{} {}", greeting, name))
    }
    
    fn main() {
        let t=|a| a+1;
        receive1(t);
        receive2(t);
        receive3(Box::new(t));
    
        println!(" {} ",return1()(String::from("jhon")));
        println!(" {} ",return2()(String::from("nerd")))
    }
    

    receive123 are trail boundaries, impl trail, and trail object

    return12 is impl trait and trait object respectively

Keywords: Rust

Added by liquidchild_au on Wed, 09 Mar 2022 10:33:03 +0200