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:

FeatureClosureFunction
AnonymityNo name; usually assigned to a variableHas a fixedfnname
Environment captureCan capture external variablesCannot capture external variables
Definition syntax|parameters| expressionfn name(parameters)
Type inferenceParameter and return types can usually be omittedMust explicitly specify parameter and return types
Storage and passingCan be used as a variable, parameter, or return valueAlso 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

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 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 methodCorresponding Rust semanticsDescription
Capture by referenceSimilar to&TDefault behavior; the closure borrows the variable, and it remains usable outside
Capture by mutable borrowSimilar to&mut TThe closure needs to modify the variable; the closure itself must be declared asmut
Capture by valueSimilar toTUsemovekeyword, moving ownership of the variable into the closure

For types that implementCopythe 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

fn main() {
    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

fn main() {
    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

fn main() {
    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.

TraitCapture methodNumber of callsTypical scenario
FnImmutable borrow (&T)Multiple timesRead-only access to captured variables
FnMutMutable borrow (&mut T)Multiple timesNeeds to modify captured variables
FnOnceTakes ownership (T)Only onceConsumes 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

fn main() {
    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

// Define a function that takes a closure as a parameter
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

// Return a closure that captures parameter x and adds it to the passed-in value
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

fn make_adder(x: i32) -> Box<dyn Fn(i32) -> 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
}
MethodAllocation locationApplicable scenarios
impl FnStackThe closure type can be determined at compile time, resulting in better performance
Box<dyn Fn>HeapDynamically 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

fn main() {
    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

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

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(|&amp;&amp;x| x > 0);
    match first_positive {
        Some(&amp;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, &amp;str>> = nums.iter().map(|&amp;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

fn main() {
    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

// Define a function that accepts a closure as a parameter
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