June 30, 20256 min

Functions or Closures in Rust? Know the Difference!

m
mayo

Understanding the distinction between functions and closures is fundamental to mastering Rust's ownership system and performance characteristics.

Closure compiles to a struct; dispatch choice sets the cost let x = 42; |y| x + y struct Closure { x: i32 } impl Fn(i32) -> i32 impl Fn (static dispatch) inlined, zero-cost recommended default Box<dyn Fn> (dynamic dispatch) vtable lookup ~2-3x slower

Key Differences

Functions Closures
Defined at compile time with fn Anonymous, created at runtime
Static dispatch (no runtime overhead) May involve dynamic dispatch (trait objects)
Cannot capture environment variables Can capture variables from enclosing scope
Always have a known type Type is unique and inferred (each closure has its own type)

Underlying Mechanics

Closures Are Structs + Traits

Rust models closures as structs that:

  • Store captured variables (as fields)
  • Implement one of the closure traits (Fn, FnMut, or FnOnce)

For example, this closure:

let x = 42;
let closure = |y| x + y;

It expands to something like:

struct AnonymousClosure {
    x: i32,  // Captured variable
}

impl FnOnce<(i32,)> for AnonymousClosure {
    type Output = i32;
    fn call_once(self, y: i32) -> i32 {
        self.x + y
    }
}

Dynamic Dispatch (Vtables)

When closures are trait objects (e.g., Box<dyn Fn(i32) -> i32>), Rust uses vtables for dynamic dispatch:

  • Vtable: A lookup table storing function pointers, enabling runtime polymorphism
  • Overhead: Indirect function calls (~2–3x slower than static dispatch)

When to Use Each

Use Functions when:

  • You need zero-cost abstractions (e.g., mathematical operations)
  • No environment capture is required
fn add(a: i32, b: i32) -> i32 { a + b }

Use Closures when:

  • You need to capture state from the environment
  • Writing short, ad-hoc logic (e.g., callbacks, iterators)
let threshold = 10;
let filter = |x: i32| x > threshold;  // Captures `threshold`
Which one do you actually need? Does the logic read a variable from the enclosing scope? no yes fn add(a: i32, b: i32) no captures, direct call, fixed type |x| x > threshold closure: unique anonymous type Is it always the same one closure at this spot? yes no impl Fn inlined, zero-cost Box<dyn Fn> vtable, heterogeneous Reach for a closure only when the answer to the first question is yes.

Performance Considerations

Scenario Static Dispatch (Closures) Dynamic Dispatch (dyn Fn)
Speed Fast (inlined) Slower (vtable lookup)
Memory No overhead Vtable + fat pointer
Use Case Hot loops, embedded Heterogeneous callbacks

Example: Static vs. Dynamic Dispatch

// Static dispatch (compile-time)
fn static_call<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x)  // Inlined
}

// Dynamic dispatch (runtime)
fn dynamic_call(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
    f(x)  // Vtable lookup
}

Key Takeaways

Functions: Predictable performance, no captures
Closures: Flexible, capture environment, but may involve vtables
🚀 Prefer static dispatch (impl Fn) unless you need trait objects

Try This: What happens if a closure captures a mutable reference and is called twice?
Answer: The borrow checker ensures exclusive access—it won't compile unless the first call completes!