Reputation: 5648
I have this rust code where I am trying to implement observability. And, in my closure add_foo_listener
I simply update the called
variable and then assert it it's called.
This code compiles, and the closure is invoked, as is evidenced by the println
, but the called
variable doesn't stick. Everything I read says I should be using FnMut
which, I believe I am. But, I don't understand why this boolean isn't being updated
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct Foo {
baz: u32
}
struct Holder {
pub foo_observers: Vec<Box<dyn FnMut(Foo)>>,
}
impl Holder {
pub fn add_foo_listener<CB: 'static + FnMut(Foo) + FnMut(Foo)>(&mut self, on_foo: CB) -> &mut Self {
self.foo_observers.push(Box::new(on_foo));
self
}
pub fn notify_foo_event(&mut self, foo: Foo) -> &mut Self {
self.foo_observers
.iter_mut()
.for_each(|notify| notify(foo.clone()));
self
}
}
#[test]
fn test_foo() -> Result<(), String> {
let mut holder = Holder{
foo_observers: Vec::new()
};
let mut called: bool = false;
let foo = Foo{
baz : 10
};
holder.add_foo_listener( move |f: Foo| {
assert_eq!(f,foo);
println!("Listener called");
called = true;
}).notify_foo_event(foo);
assert!(called); // called is still false
Ok(())
}
Output:
---- notifier::test::test_foo stdout ----
Listener called
thread 'notifier::test::test_foo' panicked at 'assertion failed: called', foo.rs
assert!(called);
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Upvotes: 0
Views: 57
Reputation: 98368
In the closure, you are using the move
keyword: this means that all the captured values are moved into the closure.
A non-Copy
value would be unusable after the closure creation, but your called
is of type bool
that is Copy
. This means that the closure has copy of called while your test_foo()
has another copy. They are disconnected, so if you modify one the other will notice nothing.
If you want to use the same value you have to somehow capture in the closure a reference to the original value. If the closure function is simple you can just remove the move
, but if it is more complex that may not be possible, then you can capture a reference explicitly:
let rcalled = &mut called;
holder.add_foo_listener(
move |f: Foo| {
println!("Listener called");
*rcalled = true;
}
).notify_foo_event(foo);
Unfortunately, this does not compile because this closure now holds a reference to a local variable, that makes it no 'static
, which is required by add_foo_listener()
.
To get a reference-like value that is 'static
you can use the Rust big-gun: Arc<RefCell<T>>
. Although in your case, since it is a Copy
value, you can use the simpler Arc<Cell<T>>
:
let called = Arc::new(Cell::new(false));
holder.add_foo_listener({
let called = called.clone();
move |f: Foo| {
println!("Listener called");
called.set(true);
}
}).notify_foo_event(foo);
assert!(called.get()); // called is now true
Upvotes: 2