That's "struct of arrays" (SoA) which is often recommended for other non-OOP reasons, and can be a case that's easier in Rust, if you want to do something like:
thread::spawn(|| do_something_to_colors(&mut owner.thing_colors));
thread::spawn(|| do_something_to_sizes(&mut owner.thing_sizes));
But that is pretty rare outside "we have ECS at home".
Here as a simple example:
// just pass this to everything with all the guts hanging out...
pub struct Context {
pub sender: Sender<Event>,
pub windows: HashMap<WindowId, Window>
pub widgets: Hashmap<WidgetId, Widget>,
// ...
}
This is something in "proper OOP style" you would wrap up the internals with a send_event(&mut self, event: Event), get_/delete_/add_widget(&mut self, ...) etc., but in Rust that &mut self parameter "locks" the entire object, which makes simple code like this fail:
// remove all widgets that are contained in windows that don't exist any more...
for (id, widget) in context.iter_widgets() {
if !context.has_window(widget.widget_id) {
context.delete_widget(id); // conflicts with earlier borrows of `context`
context.send_event(Event::WidgetDeleted(id)); // same
}
}
With it all public, you can directly access the fields, and the compiler can see they don't conflict:
// Updating a collection while iterating is always a conflict, but the common cases
// have helper methods...
context.widgets.retain(|id, widget| {
if context.windows.has(widget.window_id) {
true
} else {
// not a conflict: borrows are for context.widgets, context.windows, etc....
context.sender.send(Event::WidgetDeleted(id));
false
}
});
This is a simple case of "separating in space", another handy trick is using "double buffered" approaches to read from the previous state and writing to/returning the next state, or doing similar to what you have with the WidgetId map and keeping multiple "slices" of the same data separated by how the code uses them. Unfortunately, these are all a bit messy to demonstrate because they are inherently very tied to the logic that's accessing them.