Files
leonardomso__rust-skills/rules/closure-static-vs-dyn.md
T
2026-06-14 19:38:45 -03:00

3.2 KiB

closure-static-vs-dyn

Accept impl Fn (generic) for hot callbacks; use &dyn Fn/Box<dyn Fn> to cut code size or to store them

Why It Matters

A generic parameter F: Fn(…) -> … (or impl Fn) monomorphizes at each call site: the compiler emits a specialized copy of the function, enabling inlining and zero-cost dispatch. The trade-off is binary bloat when many different closure types are substituted. &dyn Fn/Box<dyn Fn> share a single compiled copy via a vtable, which reduces code size and is the only option for storing heterogeneous closures (e.g. an event handler registry). Choose by profiling requirements, not habit.

Bad

// Storing closures generically in a struct is impossible — the struct
// would need a type parameter per handler, making it unusable.
struct BadRegistry<F: Fn(&str)> {
    // Can only hold ONE concrete closure type — defeats the purpose.
    handler: F,
}

// Equally, using Box<dyn Fn> on a hot, single-call-site inner loop
// pays a vtable cost for no benefit.
fn transform_slow(xs: &[i32], f: &dyn Fn(i32) -> i32) -> Vec<i32> {
    xs.iter().map(|&x| f(x)).collect()
}

Good

// Generic / static dispatch: preferred for hot paths — inlinable, zero allocation.
fn transform<F: Fn(i32) -> i32>(xs: &[i32], f: F) -> Vec<i32> {
    xs.iter().map(|&x| f(x)).collect()
}

// Dynamic dispatch: required when storing heterogeneous closures.
struct Registry {
    handlers: Vec<Box<dyn Fn(&str)>>,
}

impl Registry {
    fn new() -> Self {
        Self { handlers: Vec::new() }
    }

    fn register(&mut self, handler: impl Fn(&str) + 'static) {
        self.handlers.push(Box::new(handler));
    }

    fn dispatch(&self, event: &str) {
        for handler in &self.handlers {
            handler(event);
        }
    }
}

fn demo() {
    // Static dispatch — the compiler may inline the closure entirely.
    let doubled = transform(&[1, 2, 3], |x| x * 2);
    assert_eq!(doubled, vec![2, 4, 6]);

    // Dynamic dispatch — one compiled copy, heterogeneous handlers.
    let mut reg = Registry::new();
    reg.register(|e| println!("logger: {e}"));
    reg.register(|e| println!("metrics: {e}"));
    reg.dispatch("user_signup");
}

Decision Table

Situation Use
Hot inner loop, single call site impl Fn / generic F: Fn
Callback stored in a struct field Box<dyn Fn>
Collection of mixed closures Vec<Box<dyn Fn(…)>>
Pass-through, one level deep, not stored &dyn Fn (avoids allocation)
Called across an await point Box<dyn Fn + Send>

Note: &dyn Fn is useful to avoid an allocation when you only need to borrow the closure for one call and do not store it. Pass &closure (reference to a stack-allocated closure) rather than boxing.

fn call_once_dyn(f: &dyn Fn() -> i32) -> i32 {
    f()
}

fn demo_ref() {
    let x = 7;
    let result = call_once_dyn(&|| x + 1);
    assert_eq!(result, 8);
}

See Also