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.
YouTip