YouTip LogoYouTip

Rust Closure

In Rust, a closure is an anonymous function that can capture and store variables from its surrounding environment. Closures allow access to variables outside their defining scope and can move or borrow them into the closure when needed. Closures are widely used in Rust for functional programming, concurrent programming, event-driven programming, and other areas. * * * ## Differences Between Closures and Functions Both closures and regular functions can encapsulate a piece of logic, but closures have one core capability that functions do notβ€”capturing variables from the external environment. The following table lists the main differences between the two: | Feature | Closure | Function | | --- | --- | --- | | **Anonymity** | No name; typically assigned to a variable | Has a fixed `fn` name | | **Environment Capture** | Can capture external variables | Cannot capture external variables | | **Definition Method** | |Parameters| Expression | fn Name(Parameters) | | **Type Inference** | Parameter and return types can usually be omitted | Must explicitly specify parameter and return types | | **Storage and Passing** | Can be used as a variable, argument, or return value | Also supported | * * * ## Declaring and Calling Closures The basic syntax for a closure is as follows: let closure_name = |parameter_list| expression or statement block; Parameters can have type annotations or can be omittedβ€”Rust’s compiler will infer them based on context. ## Example fn main(){ // Closure: Type annotations omitted; compiler infers automatically let add_one =|x| x +1; println!("add_one(4) = {}", add_one(4));// Output: 5 // Closure: Explicitly specifying parameter and return types let multiply =|a:i32, b:i32|->i32{ a * b }; println!("multiply(3, 7) = {}", multiply(3,7));// Output: 21 // Multi-line closures require curly braces let greet =|name:&str|{ let greeting = format!("Hello, {}!", name); greeting }; println!("{}", greet(""));// Output: Hello, ! } The calling method for closures is exactly the same as for regular functionsβ€”just append parentheses after the variable name and pass in arguments. * * * ## Capturing External Variables The most essential feature of closures is their ability to capture variables from their surrounding scope. This is the fundamental difference between closures and regular functions. Closures can capture external variables in three ways: | Capture Mode | Corresponding Rust Semantics | Explanation | | --- | --- | --- | | **Capture by Reference** | Similar to `&T` | Default behavior; the closure borrows the variable, and the external scope can still use it | | **Capture by Mutable Borrow** | Similar to `&mut T` | The closure needs to modify the variable; the closure itself must be declared as `mut` | | **Capture by Value** | Similar to `T` | Use the `move` keyword to transfer ownership of the variable into the closure | > For types that implement the `Copy` trait (such as `i32`, `bool`), `move` only makes a copy of the value, and the external variable remains usable. Therefore, when demonstrating ownership transfer, you should choose non-`Copy` types like `String`, `Vec`, etc. ### Capture by Reference By default, closures borrow external variables using immutable references. After the borrowing ends, the external scope can continue to use the variable. ## Example fn main(){ let text = String::from(""); // The closure captures text by reference without taking ownership let print_text =|| println!("text = {}", text); print_text();// Output: text = // The borrowing has ended, and the external scope can still use text println!("External scope can still use: {}", text); } ### Capture by Mutable Borrow If the closure needs to modify the captured variable, Rust will capture it using mutable references. In this case, the closure itself must be declared as `mut`. ## Example fn main(){ let mut counter =0; // The closure captures counter by mutable reference; the closure itself must also be declared as mut let mut inc =||{ counter +=1; }; inc(); inc(); println!("counter = {}", counter);// Output: counter = 2 } ### Capture by Value (Move) By adding the `move` keyword before the closure, the closure takes ownership of the captured variable. After ownership is transferred, the external scope can no longer use the variable. ## Example fn main(){ let owned = String::from(""); // move transfers ownership of owned into the closure let take_owned = move || println!("owned = {}", owned); take_owned();// Output: owned = // If you uncomment the following line, it will cause a compilation error: ownership of owned has been moved into the closure // println!("{}", owned); } When you need to pass a closure to another thread or return it out of scope, `move` is especially usefulβ€”it ensures that the closure holds ownership of the required data and avoids dangling references. * * * ## Closure Traits: Fn / FnMut / FnOnce The Rust compiler automatically implements corresponding traits for closures based on how they capture variables. Understanding these three traits is key to using closures as function arguments or return values. | Trait | Capture Mode | Number of Callable Times | Typical Scenarios | | --- | --- | --- | --- | | `Fn` | Immutable borrow (`&T`) | Multiple times | Read-only access to captured variables | | `FnMut` | Mutable borrow (`&mut T`) | Multiple times | Need to modify captured variables | | `FnOnce` | Take ownership (`T`) | Only once | Consumes captured variables; cannot be called again afterward | The inheritance relationship among these three traits is: `Fn` is a subtrait of `FnMut`, and `FnMut` is a subtrait of `FnOnce`. That means if a closure implements `Fn`, it automatically also implements `FnMut` and `FnOnce`. > Note: The `move` keyword only forces ownership transfer; it doesn’t necessarily mean the closure is `FnOnce`. If the closure body does not consume the captured value, even with `move`, the closure may still implement `Fn` (callable multiple times). ## Example fn main(){ let name = String::from(""); // Fn closure: reads but does not modify; callable multiple times let greet =|| println!("Hello, {}!", name); greet();// First call greet();// Second call, still works fine // FnMut closure: needs to modify captured variables let mut count =0; let mut increment =||{ count +=1; count }; println!("increment() = {}", increment());// Output: 1 println!("increment() = {}", increment());// Output: 2 // FnOnce closure: consumes captured variables; can only be called once let data = String::from(""); let consume = move ||{ let _ = data;// Move data out of the closure's scope, consuming it println!("data has been consumed"); }; consume(); // consume(); // If you uncomment this line, it will cause a compilation error: FnOnce closures can only be called once } * * * ## Closures as Arguments and Return Values Closures can be passed as function arguments or returned as function results. These are the two most common uses of closures in practical development. ### Closures as Arguments When passing a closure as an argument, you need to use generic constraints to specify the closure’s trait type. In the example below, the parameter `F` is constrained to `Fn(i32) -> i32`, meaning it accepts an `i32` argument and returns an `i32`. ## Example // Define a function that accepts a closure as an argument fn apply(val:i32, f: F)->i32 where F: Fn(i32)->i32,// F must implement Fn(i32) -> i32 { f(val) } fn main(){ let double =|x| x *2; let result = apply(5, double); println!("Result: {}", result);// Output: Result: 10 // You can also directly pass an anonymous closure let result2 = apply(3,|x| x +100); println!("Result2: {}", result2);// Output: Result2: 103 } ### Closures as Return Values Since the type of a closure is anonymous, you need to use `impl Trait` or `Box` to describe the return type when returning a closure. #### Using `impl Fn` to Return a Closure When the return closure type can be determined at compile time, you can use `impl Fn` without heap allocation. ## Example // Return a closure that captures the parameter x and adds it to the incoming value fn make_adder(x:i32)->impl Fn(i32)->i32{ // Must use move; otherwise, x is a local reference, and the closure will become invalid after returning move |y| x + y } fn main(){ let add_five = make_adder(5); println!("5 + 3 = {}", add_five(3));// Output: 5 + 3 = 8 let add_ten = make_adder(10); println!("10 + 2 = {}", add_ten(2));// Output: 10 + 2 = 12 } #### Using `Box` to Return a Closure When you need to dynamically select different closures at runtime, use `Box` to allocate the closure onto the heap. ## Example fn make_adder(x:i32)-> Box i32>{ Box::new(move |y| x + y) } fn main(){ let add_ten = make_adder(10); println!("10 + 2 = {}", add_ten(2));// Output: 10 + 2 = 12 } | Method | Allocation Location | Applicable Scenario | | --- | --- | --- | | `impl Fn` | Stack | Closure type can be determined at compile time; better performance | | `Box` | Heap | Dynamically select closures at runtime or need to store closures across functions | * * * ## Common Application Scenarios Closures are ubiquitous in everyday Rust development. Here are some of the most typical application scenarios. ### Closures in Iterators Closures are often used together with iterator methods to process collection elements in batches. ## Example fn main(){ let nums = vec![1,2,3,4,5]; // map: Transform each element let squared: Vec= nums.iter().map(|x| x * x).collect(); println!("Squares: {:?}", squared);// Output: [1, 4, 9, 16, 25] // filter: Filter elements that meet the condition let even: Vec= nums.iter().filter(|x|*x %2==0).collect(); println!("Even numbers: {:?}", even);// Output: [2, 4] // fold: Reduce the collection into a single value let sum:i32= nums.iter().fold(0,|acc, x| acc + x); println!("Sum: {}", sum);// Output: 15 } ### Closures and Multithreading In multithreaded programming, closures are often used to define the execution body of threads. The `move` keyword is almost always necessary in this scenario because it moves ownership of data into the new thread, avoiding dangling references across threads. ## Example use std::thread; fn main(){ let nums = vec![1,2,3,4,5]; // Create a thread for each number; move transfers ownership of num into the thread let handles: Vec= nums.into_iter().map(|num|{ thread::spawn(move ||{ num *2 }) }).collect(); // Wait for all threads to finish and collect results for handle in handles { let result = handle.join().unwrap(); println!("Result: {}", result); } } ### Closures and Error Handling Closures can return `Result` or `Option` types, combined with iterator methods to implement concise error-handling logic. ## Example fn main(){ let nums = vec![3,-1,4,-5,9]; // Use a closure to find the first positive number let first_positive = nums.iter().find(|&&x| x >0); match first_positive { Some(&n)=> println!("First positive number: {}", n),// Output: First positive number: 3 None => println!("No positive numbers"), } // Use a closure to filter and transform, combined with Result to handle possible errors let results: Vec<Result>= nums.iter().map(|&n|{ if n >0{ Ok(n *10) }else{ Err("Negative numbers cannot be processed") } }).collect(); for r in results { match r { Ok(v)=> println!("Success: {}", v), Err(e)=> println!("Error: {}", e), } } } * * * ## Performance and Lifetimes ### Performance of Closures Rust closures are lightweight. The compiler performs inlining optimizations on closures, making the overhead of calling a closure nearly equivalent to calling a regular function directly. Closures themselves do not introduce extra virtual function calls or heap allocations (unless explicitly using `Box`). ### Closures and Lifetimes The lifetime of a closure is closely related to the variables it captures. Rust’s lifetime system ensures that a closure will not outlive any of the variables it capturesβ€”if the closure references a local variable, the compiler will prevent you from returning the closure out of the variable’s scope at compile time. ## Example fn main(){ let text = String::from(""); // Correct: The closure uses text within its scope let print_text =|| println!("{}", text); print_text();// Output: // If you try to return this closure, the compiler will report an error: // fn make_closure() -> impl Fn() { // let text = String::from(""); // || println!("{}", text) // Error: text’s lifetime is too short // } // Solution: Use move to transfer ownership into the closure } * * * ## Complete Example The following example comprehensively demonstrates the core usage of closures: declaring them, capturing external variables, and passing them as arguments. ## Example // Define a function that accepts a closure as an argument fn apply_operation(num:i32, operation: F)->i32 where F: Fn(i32)->i32, { operation(num) } fn main(){ let num =5; // Define a closure: square the number let square =|x| x * x; // Pass the closure as an argument to the function let result = apply_operation(num, square); println!("Square of {} is {}", num, result);// Output: Square of 5 is 25 // You can also directly pass an anonymous closure let result2 = apply_operation(num,|x| x * x * x); println!("Cube of {} is {}", num, result2);// Output: Cube of 5 is 125 } Running this program produces the following output: Square of 5 is 25Cube of 5 is 125 * * * ## Summary Rust closures are a powerful abstraction that provide a flexible and expressive way to encapsulate logic. Closures can capture environment variables and can be passed as arguments or returned as values. Combined with iterators, closures make it easy to implement complex data processing tasks. Rust’s closure design balances safety, performance, and lifetimesβ€”the compiler ensures at compile time that closures won’t reference invalid variables and guarantees zero-overhead abstraction through inlining optimizations.
← Rust Async AwaitUndefined Behavior β†’