12.1 Defining and Using Closures
A closure is essentially a function you can define inline, without a name, which automatically “closes over” or captures variables from its surrounding environment. A closure definition begins with vertical pipes (|...|
) enclosing the parameters and can appear anywhere an expression is valid. Because it is an expression, you can store it in a variable, return it from a function, or pass it to another function—just like any other value. Closures are called using the standard function call syntax ()
.
Key Characteristics:
- Anonymous: Closures don’t require a name, though they can be assigned to variables.
- Environment Capture: They can access variables from the scope where they are created.
- Concise Syntax: Parameter and return types can often be inferred.
12.1.1 Syntax: Closures vs. Functions
While similar, closures have a more flexible syntax than named functions.
Named Function Syntax:
#![allow(unused)] fn main() { fn add(x: i32, y: i32) -> i32 { x + y } }
Closure Syntax:
#![allow(unused)] fn main() { let add = |x: i32, y: i32| -> i32 { x + y }; // Called like a function: add(5, 3) }
If the closure body is a single expression, the surrounding curly braces {}
are optional:
fn main() { let square = |x: i64| x * x; // Braces omitted println!("Square of {}: {}", 7, square(7)); // Output: Square of 7: 49 }
A closure taking no arguments uses empty pipes ||
as the syntax element identifying it as a closure with zero parameters:
fn main() { let message = "Hello!"; let print_message = || println!("{}", message); // Captures 'message' print_message(); // Output: Hello! }
Parameter and return types can often be omitted if the compiler can infer them:
fn main() { let add_one = |x| x + 1; // Types inferred (likely i32 -> i32 here) let result = add_one(5); println!("Result: {}", result); // Output: Result: 6 }
Key Differences Summarized:
Aspect | Function | Closure |
---|---|---|
Name | Mandatory (fn my_func(...) ) | Optional (can assign to let my_closure = ... ) |
Parameter / Return Types | Must be explicit | Inferred when possible |
Environment Capture | Not allowed | Automatic by reference, mutable ref, or move |
Implementation Details | Standalone code item | A struct holding captured data + code logic |
Associated Traits | Can implement Fn* traits if sig matches | Automatically implements one or more Fn* traits |
12.1.2 Environment Capture
Closures can use variables defined in their surrounding scope. Rust determines how to capture based on how the variable is used inside the closure body, choosing the weakest (least restrictive) mode necessary (Fn
> FnMut
> FnOnce
; borrow > mutable borrow > move).
fn main() { let factor = 2; // Captured by immutable reference (&factor) for Fn let mut count = 0; // Captured by mutable reference (&mut count) for FnMut let data = vec![1, 2]; // Moved (data) into closure for FnOnce let multiply_by_factor = |x| x * factor; // Implements Fn, FnMut, FnOnce let mut increment_count = || { // Implements FnMut, FnOnce count += 1; println!("Count: {}", count); }; let consume_data = || { // Implements FnOnce println!("Data length: {}", data.len()); drop(data); }; println!("Result: {}", multiply_by_factor(10)); // Output: Result: 20 increment_count(); // Output: Count: 1 increment_count(); // Output: Count: 2 consume_data(); // Output: Data length: 2 // consume_data(); // Error: cannot call FnOnce closure twice // println!("{:?}", data); // Error: data was moved // Borrowing rules apply: While 'increment_count' holds a mutable borrow // of 'count', 'count' cannot be accessed immutably or mutably elsewhere. // The borrow ends when 'increment_count' is no longer in use. println!("Final factor: {}", factor); // OK: factor was immutably borrowed println!("Final count: {}", count); // OK: mutable borrow ended }
Closures capture only the data they actually need. If a closure uses a field of a struct, only that field might be captured, especially with the move
keyword (see Section 12.5.2). Standard borrowing rules apply: if a closure captures a variable mutably, the original variable cannot be accessed in the enclosing scope while the closure holds the mutable borrow.
12.1.3 Closures are First-Class Citizens
Like functions, closures are first-class values in Rust: they can be assigned to variables, passed as arguments, returned from functions, and stored in data structures.
fn main() { // Assign closure to a variable let square = |x: i32| x * x; println!("Square of 5: {}", square(5)); // Output: Square of 5: 25 // Pass the closure variable to an iterator adapter. // Since numbers.iter() yields &i32, but square expects i32, // we use a new closure |&x| square(x) to adapt. // The |&x| pattern automatically dereferences the reference from the iterator. let numbers = vec![1, 2, 3]; let squares: Vec<_> = numbers.iter().map(|&x| square(x)).collect(); println!("Squares: {:?}", squares); // Output: Squares: [1, 4, 9] }
12.1.4 Comparison with C and C++
In C, simulating closures requires function pointers plus a void*
context, demanding manual state management and lacking type safety. C++ lambdas ([capture](params){body}
) are syntactically similar to Rust closures but rely on C++’s memory rules. Rust closures integrate directly with the ownership and borrowing system, ensuring memory safety at compile time.