Skip to content

Rust Closures

Closures are a powerful and flexible functional programming feature in Rust. They can capture variables from their surrounding environment and can be passed as parameters to other functions. This tutorial will provide an in-depth introduction to various characteristics and use cases of Rust closures.

🎯 Learning Objectives

Through this tutorial, you will master:

  • Basic concepts and syntax of closures
  • Three capture modes of closures
  • Closure type inference and type annotations
  • Applications of closures in actual programming
  • Differences between closures and function pointers
  • Higher-order functions and functional programming patterns

📖 What are Closures?

Closure Definition

Closures are anonymous functions that can capture variables from the environment where they are defined. Unlike ordinary functions, closures can access variables within the scope where they are defined. This characteristic is called "capturing."

Closures vs Functions Comparison

FeatureFunctionClosure
Syntaxfn name(params) -> return_type { body }`
Environment CaptureCannot capture environment variablesCan capture environment variables
Type InferenceRequires explicit type annotationSupports type inference
StorageFunction pointerThree different trait objects
PerformanceZero-cost abstractionDifferent overhead based on capture method

🔧 Closure Basic Syntax

Basic Syntax Forms

rust
fn main() {
    // Simplest closure - no parameters
    let greet = || println!("Hello, world!");
    greet();

    // Closure with one parameter
    let square = |x| x * x;
    println!("5 squared is: {}", square(5));

    // Closure with multiple parameters
    let add = |a, b| a + b;
    println!("3 + 4 = {}", add(3, 4));

    // Closure with code block
    let complex_calculation = |num| {
        println!("Calculating complex operation for {}...", num);
        std::thread::sleep(std::time::Duration::from_millis(100));
        num * num + num * 2 + 1
    };

    println!("Complex calculation result: {}", complex_calculation(10));

    // Closure with explicit type annotations
    let typed_closure = |x: i32| -> i32 {
        x * 2
    };

    println!("Typed closure result: {}", typed_closure(7));
}

Type Inference Example

rust
fn main() {
    // Rust will infer the closure's type based on usage
    let calculator = |num| num + 1;

    // First call determines the parameter and return value types
    let result1 = calculator(5i32);  // Inferred as i32
    println!("Result1: {}", result1);

    // Subsequent calls must use the same type
    let result2 = calculator(10);    // Must also be i32
    println!("Result2: {}", result2);

    // Different closures can have different types
    let float_calculator = |num: f64| num * 2.0;
    println!("Float result: {}", float_calculator(3.14));
}

📦 Closure Environment Capture

Three Capture Modes

Rust closures have three ways to capture environment variables, corresponding to three traits:

  1. FnOnce - Takes ownership (move)
  2. FnMut - Mutable borrow
  3. Fn - Immutable borrow
rust
fn main() {
    println!("=== Fn trait example (immutable borrow) ===");
    {
        let message = String::from("Hello");

        // Closure only reads variable, implements Fn trait
        let read_closure = || {
            println!("Read message: {}", message);
        };

        read_closure(); // Can be called multiple times
        read_closure();

        // Original variable is still available
        println!("Original message still available: {}", message);
    }

    println!("\n=== FnMut trait example (mutable borrow) ===");
    {
        let mut counter = 0;

        // Closure modifies variable, implements FnMut trait
        let mut increment = || {
            counter += 1;
            println!("Counter value: {}", counter);
        };

        increment(); // Can be called multiple times
        increment();
    }

    println!("\n=== FnOnce trait example (take ownership) ===");
    {
        let data = vec![1, 2, 3, 4, 5];

        // Closure takes ownership of variable, implements FnOnce trait
        let consume_closure = || {
            println!("Consuming data: {:?}", data);
            data // Returns data, transfers ownership
        };

        let returned_data = consume_closure(); // Can only be called once
        println!("Returned data: {:?}", returned_data);
    }
}

move Keyword

rust
use std::thread;

fn main() {
    println!("=== Without move ===");
    {
        let number = 42;

        // Without move, closure borrows variable
        let closure1 = || println!("Borrowed number: {}", number);
        closure1();

        // Original variable is still available
        println!("Original number: {}", number);
    }

    println!("\n=== With move ===");
    {
        let text = String::from("Important data");

        // Use move to force closure to take ownership
        let move_closure = move || {
            println!("Moved text: {}", text);
        };

        move_closure();

        // Original variable is no longer available
        // println!("{}", text); // Compile error
    }

    println!("\n=== Using move in thread ===");
    {
        let shared_data = vec![1, 2, 3, 4, 5];

        // Using closure in new thread must use move
        let handle = thread::spawn(move || {
            for num in &shared_data {
                println!("Number in thread: {}", num);
            }
        });

        // Wait for thread to complete
        handle.join().unwrap();
    }
}

🏗️ Higher-Order Functions and Functional Programming

Functions That Accept Closures as Parameters

rust
// Define a higher-order function that accepts a closure
fn apply_operation<F>(num: i32, operation: F) -> i32
where
    F: Fn(i32) -> i32,
{
    operation(num)
}

// More complex higher-order function
fn filter_and_map<T, U, P, M>(collection: Vec<T>, predicate: P, mapper: M) -> Vec<U>
where
    P: Fn(&T) -> bool,
    M: Fn(T) -> U,
{
    collection
        .into_iter()
        .filter(predicate)
        .map(mapper)
        .collect()
}

fn main() {
    let original_num = 10;

    // Use different closure operations
    let square_result = apply_operation(original_num, |x| x * x);
    let cube_result = apply_operation(original_num, |x| x * x * x);

    println!("Original number: {}", original_num);
    println!("Square result: {}", square_result);
    println!("Cube result: {}", cube_result);

    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Filter even numbers and square them
    let processed = filter_and_map(
        numbers.clone(),
        |&x| x % 2 == 0,  // Filter even numbers
        |x| x * x,        // Square
    );

    println!("Original data: {:?}", numbers);
    println!("Even squares: {:?}", processed);
}

Functions That Return Closures

rust
// Functions that return closures need to use Box<dyn Fn>
fn create_multiplier(multiplier: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x * multiplier)
}

// Create a more complex closure generator
fn create_accumulator(initial_value: i32) -> Box<dyn FnMut(i32) -> i32> {
    let mut accumulated = initial_value;
    Box::new(move |increment| {
        accumulated += increment;
        accumulated
    })
}

fn main() {
    let triple_multiplier = create_multiplier(3);
    let quintuple_multiplier = create_multiplier(5);

    for i in 1..=5 {
        println!("{} × 3 = {}, {} × 5 = {}",
                i, triple_multiplier(i), i, quintuple_multiplier(i));
    }

    let mut accumulator = create_accumulator(0);

    for increment in 1..=5 {
        let result = accumulator(increment);
        println!("Accumulator current value: {}", result);
    }
}

🚀 Practical Application Scenarios

Collection Operations

rust
fn main() {
    let student_scores = vec![
        ("Zhang San", 85),
        ("Li Si", 92),
        ("Wang Wu", 78),
        ("Zhao Liu", 96),
        ("Qian Qi", 88),
    ];

    // Filter passing students
    let passing_students: Vec<_> = student_scores
        .iter()
        .filter(|(_, score)| **score >= 80)
        .collect();

    println!("Passing students:");
    for (name, score) in passing_students {
        println!("{}: {}", name, score);
    }

    // Calculate average score
    let average: f64 = student_scores
        .iter()
        .map(|(_, score)| **score as f64)
        .sum::<f64>() / student_scores.len() as f64;

    println!("Average score: {:.2}", average);

    // Find highest scoring student
    let top_student = student_scores
        .iter()
        .max_by_key(|(_, score)| *score);

    if let Some((name, score)) = top_student {
        println!("Top student: {} ({} points)", name, score);
    }
}

Event Handling System

rust
use std::collections::HashMap;

// Simple event system
struct EventSystem {
    listeners: HashMap<String, Vec<Box<dyn Fn(&str)>>>,
}

impl EventSystem {
    fn new() -> Self {
        EventSystem {
            listeners: HashMap::new(),
        }
    }

    fn add_listener<F>(&mut self, event_name: &str, callback: F)
    where
        F: Fn(&str) + 'static,
    {
        self.listeners
            .entry(event_name.to_string())
            .or_insert_with(Vec::new)
            .push(Box::new(callback));
    }

    fn trigger_event(&self, event_name: &str, data: &str) {
        if let Some(callbacks) = self.listeners.get(event_name) {
            for callback in callbacks {
                callback(data);
            }
        }
    }
}

fn main() {
    let mut event_system = EventSystem::new();

    // Add user login event listener
    event_system.add_listener("user_login", |username| {
        println!("📝 Log: User {} has logged in", username);
    });

    event_system.add_listener("user_login", |username| {
        println!("📧 Email: Send welcome email to {}", username);
    });

    // Simulate event trigger
    event_system.trigger_event("user_login", "Zhang San");
}

📚 Summary

This tutorial comprehensively introduced the core concepts and practical applications of Rust closures:

Main Content Review

  1. Closure Basics: Understand the concepts, syntax, and type inference of closures
  2. Environment Capture: Master three capture modes (Fn, FnMut, FnOnce)
  3. move Keyword: Learn scenarios for forced ownership taking
  4. Higher-Order Functions: Functional programming patterns that accept and return closures
  5. Practical Applications: Collection operations, event handling, etc.

Key Concepts Summary

ConceptCharacteristicsUse Cases
FnImmutable borrow of environmentMultiple call read-only operations
FnMutMutable borrow of environmentNeed to modify captured variables
FnOnceTake environment ownershipOnly call once or consume captured variables
moveForce ownership takingPass data between threads

Advantages of Closures

  • Flexibility: Can be defined when needed without separate naming
  • Environment Capture: Can access external variables, reducing parameter passing
  • Type Inference: Reduces explicit type annotations
  • Inline Optimization: Compiler can better optimize closures

Practice Suggestions

  • Prioritize using Fn trait, use FnMut or FnOnce only when needed
  • Use move keyword in multithreaded environments
  • Fully utilize the conciseness of closures in collection operations
  • Use closures to implement event-driven programming patterns

Considerations

  • Pay attention to closure lifetime and memory usage
  • Avoid capturing too many unnecessary variables in closures
  • Consider performance impact in high-frequency call scenarios
  • Complex closures may affect code readability

Closures are an important feature of Rust functional programming. Mastering the use of closures will significantly improve your Rust programming efficiency and code expressiveness. Through this tutorial, you should now be able to flexibly use closures to solve various programming problems in actual projects.


Continue learning: Next chapter - Rust Ownership

Content is for learning and research only.