Rust Closures
A closure in Rust is an anonymous function that can capture and store variables from its environment.
Closures allow variables to be accessed outside the scope in which they are defined, and can move or borrow them into the closure when needed.
Closures are widely used in Rust in areas such as functional programming, concurrent programming, and event-driven programming.
Difference Between Closures and Functions
Both closures and ordinary functions can encapsulate a piece of logic, but closures have an additional core capability: capturing variables from the external environment. The following table lists the main differences between the two:
| Feature | Closure | Function |
|---|---|---|
| Anonymity | No name; usually assigned to a variable | Has a fixedfnname |
| Environment capture | Can capture external variables | Cannot capture external variables |
| Definition syntax | |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, parameter, or return value | Also supported |
Declaring and Calling Closures
The basic syntax of a closure is as follows:
let closure_name = |参数列表| 表达式或语句块;
Parameters can have type annotations, or they can be omitted — the Rust compiler infers them from the context.
Example
// Closure: type annotations omitted, compiler infers automatically
let add_one = |x| x + 1;
println!("add_one(4) = {}", add_one(4)); // Output: 5
// Closure: explicitly annotate parameter and return types
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("multiply(3, 7) = {}", multiply(3, 7)); // Output: 21
// Multi-line closure requires braces
let greet = |name: &str| {
let greeting = format!("Hello, {}!", name);
greeting
};
println!("{}", greet("example")); // Output: Hello, example!
}
Closures are called in exactly the same way as ordinary functions: add parentheses after the variable name and pass the arguments.
Capturing External Variables
The most essential characteristic of a closure is its ability to capture variables from the scope in which it is defined. This is the fundamental difference between closures and ordinary functions.
Closures can capture external variables in three ways:
| Capture method | Corresponding Rust semantics | Description |
|---|---|---|
| Capture by reference | Similar to&T | Default behavior; the closure borrows the variable, and it remains usable outside |
| Capture by mutable borrow | Similar to&mut T | The closure needs to modify the variable; the closure itself must be declared asmut |
| Capture by value | Similar toT | Usemovekeyword, moving ownership of the variable into the closure |
For types that implement
Copythe trait (such asi32、bool),moveonly a copy of the value is made, and the external variable remains usable. Therefore, when demonstrating ownership transfer, chooseString、Vecand other non-Copytypes.
Capture by Reference
By default, a closure borrows external variables by immutable reference. After the borrow ends, the external scope can continue to use the variable.
Example
let text = String::from("example");
// The closure captures text by reference and does not take ownership
let print_text = || println!("text = {}", text);
print_text(); // Output: text = example
// The borrow has ended; text can still be used outside
println!("Still usable outside: {}", text);
}
Capture by Mutable Borrow
If a closure needs to modify a captured variable, Rust captures it by mutable reference. In this case, the closure itself must be declared asmut。
Example
let mut counter = 0;
// The closure captures counter by &mut; 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 themovekeyword before the closure, the closure takes ownership of the captured variables. After ownership is transferred, the external scope can no longer use the variable.
Example
let owned = String::from("EXAMPLE");
// move transfers ownership of owned into the closure
let take_owned = move || println!("owned = {}", owned);
take_owned(); // Output: owned = EXAMPLE
// If you uncomment the line below, it will fail to compile: ownership of owned has been moved into the closure
// println!("{}", owned);
}
When a closure needs to be passed to another thread or returned outside its scope,moveit is especially common — it ensures the closure owns the required data and prevents dangling references.
Closure Traits: Fn / FnMut / FnOnce
The Rust compiler automatically implements the corresponding trait for a closure based on how it captures variables. Understanding these three traits is the key to using closures as function parameters or return values.
| Trait | Capture method | Number of calls | Typical scenario |
|---|---|---|---|
Fn | Immutable borrow (&T) | Multiple times | Read-only access to captured variables |
FnMut | Mutable borrow (&mut T) | Multiple times | Needs to modify captured variables |
FnOnce | Takes ownership (T) | Only once | Consumes captured variables; cannot be called again afterward |
The inheritance relationship among the three is:FnYesFnMutis a subtrait of,FnMutYesFnOnceis a subtrait of. That is, a closure that implementsFnautomatically also implementsFnMutandFnOnce。
Note:
moveThe keyword merely forces ownership to be taken; it does not mean the closure must beFnOnce. If the closure body does not consume the captured value, even ifmoveis added, the closure may still implementFn(can be called multiple times).
Example
let name = String::from("example");
// Fn closure: only reads, does not modify, and can be called 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("EXAMPLE");
let consume = move || {
let _ = data; // Moves data out of the closure environment, consuming it
println!("data has been consumed");
};
consume();
// consume(); // If uncommented, compilation fails: FnOnce closures can only be called once
}
Closures as Parameters and Return Values
Closures can be passed as function parameters and returned as function return values. These are the two most common uses of closures in real-world development.
Closures as Parameters
When using a closure as a parameter, you need to specify the closure's trait type with a generic bound. In the following example, the parameterFis constrained toFn(i32) -> i32, meaning a closure that takes ai32parameter and returnsi32a value.
Example
fn apply<F>(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 pass an anonymous closure directly
let result2 = apply(3, |x| x + 100);
println!("Result2: {}", result2); // Output: Result2: 103
}
Closures as Return Values
Since a closure's type is anonymous, returning a closure requiresimpl TraitorBox<dyn Trait>to describe the return type.
Returning a Closure with impl Fn
When the type of the returned closure can be determined at compile time, useimpl Fnand no heap allocation is needed.
Example
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// Must use move; otherwise x is a local reference, and x becomes invalid after the closure is returned
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
}
Use Box<dyn Fn> to return closures
When you need to dynamically select different closures to return at runtime, useBox<dyn Fn>Allocate the closure on the heap.
Example
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 scenarios |
|---|---|---|
impl Fn | Stack | The closure type can be determined at compile time, resulting in better performance |
Box<dyn Fn> | Heap | Dynamically select closures at runtime, or need to store closures across functions |
Common Use Cases
Closures are ubiquitous in Rust's daily development. Here are several of the most typical application scenarios.
Closures in Iterators
Closures are often used together with iterator methods to process collection elements in batches.
Example
let nums = vec![1, 2, 3, 4, 5];
// map: transform each element
let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
println!("squares: {:?}", squared); // Output: [1, 4, 9, 16, 25]
// filter: filter elements that satisfy the condition
let even: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
println!("evens: {:?}", even); // Output: [2, 4]
// fold: reduce a collection to 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 a thread.moveThe `move` keyword is almost essential in this scenario, because it moves ownership of the data into the new thread, preventing cross-thread references from becoming invalid.
Example
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 complete and collect the results
for handle in handles {
let result = handle.join().unwrap();
println!("Result: {}", result);
}
}
Closures and Error Handling
Closures can returnResultorOptiontypes, which combined with iterator methods implement concise error handling logic.
Example
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<i32, &str>> = 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's closures are lightweight. The compiler performs inlining optimizations on closures, making the overhead of closure calls close to directly calling ordinary functions. Closures themselves do not introduce extra virtual function calls or heap allocations (unless explicitly usingBox)。
Closures and Lifetimes
The lifetime of a closure is closely related to the variables it captures. Rust's lifetime system ensures that a closure does not outlive any variable it captures—if a closure references a local variable, the compiler will prevent you from returning the closure to outside that variable's scope at compile time.
Example
let text = String::from("example");
// Correct: use the closure within the scope of text
let print_text = || println!("{}", text);
print_text(); // Output: example
// If you try to return this closure, the compiler will report an error:
// fn make_closure() -> impl Fn() {
// let text = String::from("example");
// || println!("{}", text) // Error: text's lifetime is not long enough
// }
// Solution: use move to transfer ownership into the closure
}
Complete Example
The following example comprehensively demonstrates core usages of closures such as declaration, capturing external variables, and passing as arguments.
Example
fn apply_operation<F>(num: i32, operation: F) -> i32
where
F: Fn(i32) -> i32,
{
operation(num)
}
fn main() {
let num = 5;
// Define a closure: square a number
let square = |x| x * x;
// Pass the closure into the function as an argument
let result = apply_operation(num, square);
println!("Square of {} is {}", num, result); // Output: Square of 5 is 25
// You can also pass an anonymous closure directly
let result2 = apply_operation(num, |x| x * x * x);
println!("Cube of {} is {}", num, result2); // Output: Cube of 5 is 125
}
Run the program; the output is as follows:
Square of 5 is 25 Cube of 5 is 125
Summary
Rust's closures are a powerful abstraction that provides a flexible and expressive way to encapsulate logic.
Closures can capture environment variables and can be passed as parameters or returned as return values. When combined with iterators, closures can conveniently implement complex data processing tasks.
Rust's closure design takes into account safety, performance, and lifetime—the compiler ensures at compile time that closures do not reference invalidated variables, and guarantees zero-cost abstraction for closure calls through inlining optimization.
Other extensions