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
| Feature | Function | Closure |
|---|---|---|
| Syntax | fn name(params) -> return_type { body } | ` |
| Environment Capture | Cannot capture environment variables | Can capture environment variables |
| Type Inference | Requires explicit type annotation | Supports type inference |
| Storage | Function pointer | Three different trait objects |
| Performance | Zero-cost abstraction | Different overhead based on capture method |
🔧 Closure Basic Syntax
Basic Syntax Forms
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
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:
- FnOnce - Takes ownership (move)
- FnMut - Mutable borrow
- Fn - Immutable borrow
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
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
// 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
// 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
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
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
- Closure Basics: Understand the concepts, syntax, and type inference of closures
- Environment Capture: Master three capture modes (Fn, FnMut, FnOnce)
- move Keyword: Learn scenarios for forced ownership taking
- Higher-Order Functions: Functional programming patterns that accept and return closures
- Practical Applications: Collection operations, event handling, etc.
Key Concepts Summary
| Concept | Characteristics | Use Cases |
|---|---|---|
| Fn | Immutable borrow of environment | Multiple call read-only operations |
| FnMut | Mutable borrow of environment | Need to modify captured variables |
| FnOnce | Take environment ownership | Only call once or consume captured variables |
| move | Force ownership taking | Pass 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
Fntrait, useFnMutorFnOnceonly when needed - Use
movekeyword 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