What Are Closures in Rust?
Closures are anonymous functions you can define inline. Unlike regular functions, closures can capture and use variables from the scope in which they are defined. They are often used when you need to pass behavior as a value or create concise, reusable logic.
Syntax of Closures
The syntax of a closure is straightforward:
- Use a pipe
|
symbol to enclose parameters. - Write the body of the closure after the parameters.
let closure_name = |parameters| expression;
Example 1: Simple Closure
fn main() {
let add = |a, b| a + b; // Closure to add two numbers
let result = add(5, 3);
println!("The sum is: {}", result);
}
Output:
The sum is: 8
Closures vs Functions
Feature | Closures | Functions |
---|---|---|
Definition | Inline, anonymous | Defined with fn keyword |
Environment Access | Can capture variables | Cannot access variables outside |
Type Inference | Parameters can be inferred | Parameters must be explicitly declared |
Capturing the Environment
Closures in Rust can capture variables from their surrounding scope in three ways:
- By Borrowing (
&T
) - By Mutably Borrowing (
&mut T
) - By Taking Ownership (
T
)
Example 2: Capturing by Borrowing
fn main() {
let x = 10;
let print_x = || println!("x is: {}", x); // Borrowing x
print_x();
}
Output:
x is: 10
Example 3: Capturing by Mutably Borrowing
fn main() {
let mut x = 5;
let mut update_x = || x += 1; // Mutably borrowing x
update_x();
println!("Updated x: {}", x);
}
Output:
Updated x: 6
Example 4: Capturing by Taking Ownership
fn main() {
let x = String::from("Rust");
let consume_x = || println!("Captured value: {}", x); // Taking ownership
consume_x();
// println!("{}", x); // Error: x is moved
}
Output:
Captured value: Rust
Closures with Explicit Types
Rust can infer parameter and return types for closures, but you can also specify them explicitly if needed.
Example 5: Explicit Type Annotations
fn main() {
let multiply = |a: i32, b: i32| -> i32 { a * b };
let result = multiply(4, 5);
println!("The product is: {}", result);
}
Output:
The product is: 20
Closures and Iterators
Closures are often used with iterators to process collections concisely.
Example 6: Using Closures with Iterators
fn main() {
let numbers = vec![1, 2, 3, 4];
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();
println!("Squares: {:?}", squares);
}
Output:
Squares: [1, 4, 9, 16]
Closures and move Keyword
The move keyword forces a closure to take ownership of the variables it captures.
Example 7: Using move with Closures
fn main() {
let x = String::from("Rust");
let print_x = move || println!("Owned value: {}", x);
print_x();
// println!("{}", x); // Error: x is moved
}
Output:
Owned value: Rust
Closures as Function Parameters
Closures can be passed to functions as arguments. You can use generics and the Fn traits (Fn, FnMut, FnOnce) to define such functions.
Example 8: Passing Closures to a Function
fn apply_closure<F>(f: F)
where
F: Fn(i32) -> i32,
{
let result = f(10);
println!("Result: {}", result);
}
fn main() {
let double = |x| x * 2;
apply_closure(double);
}
Output:
Result: 20
Common Traits for Closures
- Fn: The closure borrows values immutably.
- FnMut: The closure mutably borrows values.
- FnOnce: The closure takes ownership of the values.
Returning Closures
Closures can also be returned from functions using the impl keyword with the appropriate Fn trait.
Example 9: Returning a Closure
fn create_multiplier(multiplier: i32) -> impl Fn(i32) -> i32 {
move |x| x * multiplier
}
fn main() {
let multiply_by_3 = create_multiplier(3);
println!("3 * 4 = {}", multiply_by_3(4));
}
Output:
3 * 4 = 12