🎓 A1Learner
🔍

Rust Callbacks in Structs

A callback is a closure stored in a struct field for later use. An event happens later, and the object runs the stored closure then.

A generic field keeps the exact closure

Name the closure kind as a type parameter, and the struct stores that one closure directly, with no box:

struct Greeter<F: Fn()> {
    action: F,
}

A boxed field takes any closure

Box<dyn FnMut()> hides the closure's own type. One struct can hold any closure with that call shape. The handler can be swapped at run time. The Option says "no handler set yet":

struct Button {
    on_click: Option<Box<dyn FnMut(u32)>>,
}

Calling an FnMut field needs &mut self

The handler may change its captures, so the method that runs it must be allowed to change the struct. The two fields are disjoint, so reading self.clicks while the handler is borrowed is fine:

fn click(&mut self) {
    self.clicks += 1;
    if let Some(handler) = &mut self.on_click {
        handler(self.clicks);
    }
}

A stored FnOnce cannot be called in place

Calling an FnOnce value consumes it — but a field behind &mut self cannot be moved out. This method does not compile:

fn fire(&mut self) {
    (self.action)(); // error E0507: cannot move out of self.action
}

Option and take() are the one-shot pattern

Store the one-shot in an Option. take() swaps the closure out and leaves None behind. The method owns it for the call. A second fire finds nothing there:

fn fire(&mut self) {
    if let Some(action) = self.action.take() {
        action();
    }
}

Example

struct Button {
    label: String,
    clicks: u32,
    on_click: Option<Box<dyn FnMut(u32)>>,
}

impl Button {
    fn new(label: &str) -> Button {
        Button {
            label: label.to_string(),
            clicks: 0,
            on_click: None,
        }
    }
    fn set_handler<F>(&mut self, handler: F)
    where
        F: FnMut(u32) + 'static,
    {
        self.on_click = Some(Box::new(handler));
    }
    fn click(&mut self) {
        self.clicks += 1;
        let clicks = self.clicks;
        if let Some(handler) = &mut self.on_click {
            handler(clicks);
        }
    }
}

struct Alarm {
    on_ring: Option<Box<dyn FnOnce(String)>>,
}

impl Alarm {
    fn ring(&mut self, room: &str) {
        if let Some(handler) = self.on_ring.take() {
            handler(room.to_string());
        } else {
            println!("(alarm already used)");
        }
    }
}

fn main() {
    let mut button = Button::new("Save");
    println!("button: {}", button.label);
    let mut seen = 0;
    button.set_handler(move |n| {
        seen += 1;
        println!("handler ran {seen} times (click {n})");
    });
    button.click();
    button.click();

    let mut alarm = Alarm {
        on_ring: Some(Box::new(|room: String| println!("ring ring in the {room}"))),
    };
    alarm.ring("kitchen");
    alarm.ring("kitchen");
}

Key points

  • A closure field needs a spelled-out type.
  • Use a generic parameter (F: FnMut...) or a box (Box<dyn FnMut...>).
  • A closure's own type has no name you could write
  • Choose a generic field for one fixed handler chosen at compile time.
  • Choose Box when handlers are swapped, collected, or supplied by other code
  • A handler stored for later must own its captures.
  • Setter methods ask for F: FnMut(...) + 'static.
  • A closure that only borrows is refused at the door
  • Match the call to the rung.
  • An Fn field can be called through &self.
  • An FnMut field needs &mut self.
  • An FnOnce field must be taken out of the struct before its single call
  • GUI buttons, timers and test mocks are all this pattern.

In this example, Button stores its handler as Option<Box<dyn FnMut(u32)>> — None until set_handler boxes a closure into it. The handler is a move closure that owns its own counter, seen. click borrows the handler mutably. It calls it with the running click count. So it prints handler ran 1 times, then handler ran 2 times. Alarm shows the one-shot form. Its FnOnce handler sits in an Option. ring takes it out and calls it — ring ring in the kitchen. The second ring finds None where the handler used to be, and prints (alarm already used).

Note

This is the shape of real event systems — a struct that remembers behaviour, and methods that trigger it.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why is a one-shot callback field written as Option<Box<dyn FnOnce()>>?