Observers and callbacks

An observer is a list of things to tell when something happens. The subject owns the list, the observers subscribe, and every event walks the list once. The pattern is the backbone of a user interface, of a configuration watcher and of a logger, and it is where ownership in Rust becomes a design question: who keeps whom alive.

This lesson keeps a list of closures, then a list of trait objects, removes one subscriber by its id, holds the observers weakly so that a dropped window stops receiving events, and ends with the closure that borrows a local and cannot be stored.

A list of callbacks

The smallest observer is a callback. The subject stores Box<dyn Fn(i32)> and calls each one with the event value. The closures may capture anything they own, which is why they are boxed and why the trait is the call signature.

The button has two listeners and one click. Both print with the value the click carried.

struct Button { listeners: Vec<Box<dyn Fn(i32)>>, } impl Button { fn new() -> Button { Button { listeners: Vec::new() } } fn on_click(&mut self, listener: Box<dyn Fn(i32)>) { self.listeners.push(listener); } fn click(&self, x: i32) { for listener in &self.listeners { listener(x); } } } fn main() { let mut button = Button::new(); button.on_click(Box::new(|x| println!("first {x}"))); button.on_click(Box::new(|x| println!("second {x}"))); button.click(7); }

rustc --edition 2024 callbacks.rs
./callbacks

first 7
second 7

A callback is also the only shape that lets the listener be an ordinary function value, which is what a test wants: a closure that pushes into a RefCell<Vec<i32>> records the events without a struct and without a trait. The subject does not know whether the listener prints, records or forwards the event, and that is the whole point of the pattern. The order is the order of subscription, and it is the only order an observer list promises. A listener added during a notification would need a copy of the list or an index, which is a decision a real event loop makes explicitly. The lesson on type erasure explains the box that makes two different closure types fit one vector.

An observer trait

A callback is enough when the reaction is a function. An observer that keeps state needs a type with a method, and the trait turns the two kinds of listener into one list again.

The subject notifies two observers with three and then four. The total prints after each addition and the log prints its whole vector.

trait Observer { fn update(&mut self, value: i32); } struct Total { sum: i32, } impl Observer for Total { fn update(&mut self, value: i32) { self.sum += value; println!("total {}", self.sum); } } struct Log { entries: Vec<i32>, } impl Observer for Log { fn update(&mut self, value: i32) { self.entries.push(value); println!("log {:?}", self.entries); } } struct Subject { observers: Vec<Box<dyn Observer>>, } impl Subject { fn new() -> Subject { Subject { observers: Vec::new() } } fn subscribe(&mut self, observer: Box<dyn Observer>) { self.observers.push(observer); } fn notify(&mut self, value: i32) { for observer in &mut self.observers { observer.update(value); } } } fn main() { let mut subject = Subject::new(); subject.subscribe(Box::new(Total { sum: 0 })); subject.subscribe(Box::new(Log { entries: Vec::new() })); subject.notify(3); subject.notify(4); }

rustc --edition 2024 trait_obs.rs
./trait_obs

total 3
log [3]
total 7
log [3, 4]

update takes &mut self, so the notification needs &mut self on the subject and a mutable iterator. An observer that needs to call back into the subject would make the borrow checker refuse, which is why a real event loop queues the events instead of recursing into the same subject.

The lesson on traits covers the default update that many observers of this shape share, and the visitor lesson shows the enum of events that a mature subject usually sends instead of a single number.

A subscriber that unsubscribes

Removing a listener means finding it in the vector again, and two closures with the same body are two different values that cannot be compared. The subject therefore hands out an id at subscription and removes by that id, which is the same handle a window toolkit returns.

The program subscribes two listeners, notifies both, removes the first and notifies again. The count in the middle is the length of the list after the removal.

struct Subject { next_id: usize, listeners: Vec<(usize, Box<dyn Fn(i32)>)>, } impl Subject { fn new() -> Subject { Subject { next_id: 1, listeners: Vec::new() } } fn subscribe(&mut self, listener: Box<dyn Fn(i32)>) -> usize { let id = self.next_id; self.next_id += 1; self.listeners.push((id, listener)); id } fn unsubscribe(&mut self, id: usize) { self.listeners.retain(|(existing, _)| *existing != id); } fn notify(&self, value: i32) { for (_, listener) in &self.listeners { listener(value); } } } fn main() { let mut subject = Subject::new(); let first = subject.subscribe(Box::new(|v| println!("a {v}"))); subject.subscribe(Box::new(|v| println!("b {v}"))); subject.notify(1); subject.unsubscribe(first); println!("remaining {}", subject.listeners.len()); subject.notify(2); }

rustc --edition 2024 unsubscribe.rs
./unsubscribe

a 1
b 1
remaining 1
b 2

A listener that removes itself from inside the notification is the case that needs care: the vector is borrowed by the loop, so the removal has to be deferred until the walk ends. A real subject collects the ids to remove into a second vector and applies them after the loop, which is also why the notification takes &self and the removal takes &mut self. retain keeps the entries whose id differs, which drops the box and frees the closure. An id that was never handed out removes nothing, and that is a property worth a test rather than an assertion. The lesson on vectors covers retain and its cost: every element is visited once.

Weak references to the observers

A long-lived subject that owns its observers keeps every window and every panel alive forever, which is the leak a graphical program notices first. The subject should observe without owning: it holds Weak handles, and an observer that is dropped elsewhere simply stops receiving events.

The registry holds weak handles to two counters revved with Rc and RefCell. One counter is dropped between the two notifications, and only the other one is updated.

use std::cell::RefCell; use std::rc::{Rc, Weak}; struct Counter { value: i32, } fn notify(listeners: &[Weak<RefCell<Counter>>], value: i32) -> usize { let mut alive = 0; for weak in listeners { if let Some(counter) = weak.upgrade() { counter.borrow_mut().value += value; alive += 1; } } alive } fn main() { let first = Rc::new(RefCell::new(Counter { value: 0 })); let second = Rc::new(RefCell::new(Counter { value: 0 })); let listeners = vec![Rc::downgrade(&first), Rc::downgrade(&second)]; println!("{}", notify(&listeners, 5)); drop(second); println!("{}", notify(&listeners, 2)); println!("{} {}", first.borrow().value, Rc::strong_count(&first)); }

rustc --edition 2024 weak.rs
./weak

2
1
7 1

Both counters are alive at the first notification, so two are updated; after the second is dropped only one handle upgrades, and the surviving counter holds seven. strong_count of one says that nothing but the local binding owns it, which is the proof that the registry did not keep it alive. The lesson on Arc and Weak covers the same pair across threads, and Rc and RefCell cover the shared mutation inside each counter.

The closure that borrows a local

A boxed callback is stored with the lifetime 'static, because the box may outlive the function that made it. A closure that borrows a local therefore cannot be stored, and the error names the local that would be dropped while the closure still points at it.

struct Button { listeners: Vec<Box<dyn Fn()>>, } fn main() { let mut button = Button { listeners: Vec::new() }; let label = String::from("ok"); button.listeners.push(Box::new(|| println!("{label}"))); (button.listeners[0])(); }

rustc --edition 2024 borrow_err.rs

error[E0597]: `label` does not live long enough
--> borrow_err.rs:8:50
|
7 | let label = String::from("ok");
| ----- binding `label` declared here
8 | button.listeners.push(Box::new(|| println!("{label}")));
| -----------------------^^^^^----
| | | |
| | | borrowed value does not live long enough
| | value captured here
| coercion requires that `label` is borrowed for `'static`
9 | (button.listeners[0])();
10 | }
| - `label` dropped here while still borrowed
|
= note: due to object lifetime defaults, `Box<dyn Fn()>` actually means `Box<(dyn Fn() + 'static)>`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0597`.

The fix is move: Box::new(move || println!("{label}")) takes the string into the closure, so the closure owns it and the local is free to end. A listener that must read shared state instead holds an Rc<RefCell<T>> and captures a clone, or a Weak as in the previous section. The weak reference lesson is the reference for the second shape, and the next lesson builds objects with many optional parts instead of notifying them.

Exercises

  1. Give the button a label field and print it in every listener through a move closure.
  2. Make the subject send an enum of two events and add an observer that reacts to one of them only.
  3. Replace the weak handles with strong ones and explain, with strong_count, what happens to a dropped counter.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does an observer subject keep?

    Answer

    A list of callbacks or trait objects, called once per event

    The list is the pattern, and every notification walks it in subscription order.

  2. Why does a subject hand out an id when a listener subscribes?

    Answer

    Because two closures cannot be compared, so the id is what a later removal names

    The id is the handle that unsubscribe uses to find the entry again.

  3. Why does a long-lived subject hold Weak handles to its observers?

    Answer

    So that the subject does not keep an observer alive after it was closed

    Ownership stays with the observer, and upgrade fails once it is gone.

  4. What does Weak::upgrade return?

    Answer

    Some(Rc) while the value lives, and None after it was dropped

    The upgrade is the only way from a weak handle to a value, and it can fail.

  5. Why must a stored listener usually be move?

    Answer

    The box may outlive the function, so the closure has to own what it captures

    Box&lt;dyn Fn()&gt; means a static closure, and a borrow of a local cannot be static.

  6. What does this program print? struct Button { listeners: Vec<Box<dyn Fn(i32)>>, } impl Button { fn click(&self, x: i32) { for listener in &self.listeners { listener(x); } } } fn main() { let button = Button { listeners: vec![Box::new(|x| println!("one {x}")), Box::new(|x| println!("two {x}"))], }; button.click(3); }

    Answer

    one 3 / two 3

    The vector keeps the order, and both listeners receive the same value.

  7. What does this program print? trait Observer { fn update(&mut self, value: i32); } struct Sum { total: i32, } impl Observer for Sum { fn update(&mut self, value: i32) { self.total += value; } } fn main() { let mut observers: Vec<Box<dyn Observer>> = vec![Box::new(Sum { total: 1 })]; for observer in &mut observers { observer.update(4); observer.update(5); } println!("ok"); }

    Answer

    ok

    The updates change the observer, and the program prints a fixed word.

  8. What does this program print? struct Subject { next_id: usize, listeners: Vec<(usize, Box<dyn Fn()>)>, } impl Subject { fn subscribe(&mut self) -> usize { let id = self.next_id; self.next_id += 1; self.listeners.push((id, Box::new(|| println!("hi")))); id } fn unsubscribe(&mut self, id: usize) { self.listeners.retain(|(existing, _)| *existing != id); } } fn main() { let mut subject = Subject { next_id: 10, listeners: Vec::new() }; let first = subject.subscribe(); subject.subscribe(); subject.unsubscribe(first); println!("{} {}", first, subject.listeners.len()); }

    Answer

    10 1

    The first id is the starting counter, and one listener remains after the removal.

  9. What does this program print? use std::cell::RefCell; use std::rc::{Rc, Weak}; fn main() { let value = Rc::new(RefCell::new(0)); let weak: Weak<RefCell<i32>> = Rc::downgrade(&value); *value.borrow_mut() += 2; let upgraded = weak.upgrade().is_some(); drop(value); println!("{} {}", upgraded, weak.upgrade().is_some()); }

    Answer

    true false

    The weak handle upgrades while the strong one lives and fails after it is dropped.

  10. What is the first line of the compiler output for this program? struct Button { listeners: Vec<Box<dyn Fn()>>, } fn main() { let mut button = Button { listeners: Vec::new() }; let label = String::from("ok"); button.listeners.push(Box::new(|| println!("{label}"))); }

    Answer

    error[E0597]: label does not live long enough

    The box needs a static closure, and the borrow of the local cannot last that long.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev