So, I'm trying to understand the logic of not allowing immutable refs at the same time as mutable refs.
An immutable read that a mutation breaks. Vec.pop() on Vec.last()
- Unbreakable reads, or reads that are not yet called.
- A model with multiple nodes, each node referencing other nodes. A caller with references to all nodes. Rc shared ownership. The model cannot mutate itself because the caller has references.
- Note: no distinction made between external reference and internal reference (aka, passive vs active reads?). Here, the caller only has node references, which because of Rc won't die. Thus there is no way that mutating the model could break the node reference, but Rust treats it so. Vec.last() produces a reference internal to model and is thus breakable.
- Workaround: use RefCell. Essentially divides "passive vs active" as "available vs borrowed". Caller only borrows target node.
- Adds a bunch of RefCell overhead.
- No longer have compile time information on if there are multiple mutable borrows at the same time. All has to be logic-ed out from a runtime perspective, even though allowing multiple mutable borrows wasn't the goal in the first place.
- Target node can mutate all other nodes, however, none of those other nodes can acquire a reference to target node in the process. This breaks things at runtime.
- Doesn't actually indicate a problem? Obviously, other nodes acquiring a mutable reference is an issue. However, say they acquire an immutable reference. Once again, there is a problem of distinguishing "passive vs active". other_node has a reference to target_node, but not to any internal target_node things. If target_node mutates something, other_node has no issue, but Rust says it does.
- An example of where this might happen: caller calls target node. Target node needs to locate a sibling that meets some criteria. Target node calls on parent to search its children. Parent has no way to exclude target node from search, and so acquires an immutable reference to target node.
- Further solutions: attach some kind of id to target node, which parent node can use to exclude from search. Or have node have references to all siblings, which are maintained by the parent. Either way, a lot of overhead for a non-issue.
- Can rectify with a deeper layer of RefCell. Make all node methods "immutable", and only need Rc pointers for calling methods on nodes. Nodes internally wrap data in RefCell, and handle all the borrowing for reading and writing. This is what slint's VecModel essentially does.
- Lost external compile time information. If all public methods are "immutable", and there is some kind of public read that returns an internal reference, then in a single thread you could mix an active read and write. If you have multiple threads, you could wrap in Arc thinking that it's fine as all methods are immutable, and break something. This can only be warned against via documentation.
- Maintenance-wise, the lack of compile time linting on the internal node methods goes deeper.
- For my use case, this seems like the best solution.
It seems like things would be a lot simpler if there was a distinction between active and passive immutable references. External vs internal? Basically if ownership applied to references.
Say that obj.read() returns a reference to a value owned by obj. Then obj.write() is called. As there exists an immutable reference to a value owned by obj, this is not allowed.
Now say that you have &obj, or an obj.read() that returns a value not owned by obj. obj.write() being called goes perfectly fine.
I think this could still be done with two keywords: &mut could also apply to immutable references that are breakable, where &muts can't coexist. But I can't just decide on this convention, as Rust would still block coexisting &mut and &.
I could also think of a couple useful variations on RefCell:
RefCell that still does compile-time checks for overlapping mutable references, but not immutables.
Could take this further, and implement the runtime checking for borrow_mut as I envisioned for &mut above. Would have to somehow mask this from Rust runtime checks though...
Combined Rc/RefCell, where you can get a Rc/RefCell that is restricted to only allow borrowing immutables. Kinda along the lines of strong-weak Rcs, except now it's about mutability access.
I'm a beginner to Rust. Am I misunderstanding something?
I'm currently trying to write a graphic program. The model is a tree of nodes. The updator has references to all the nodes, but no knowledge of the structure of the nodes. It can read a node to update the ui. It can update a node, and the node may need to propagate the change to other attached nodes. I'm in RefCell hell right now, so if you have any suggestions as to how you would tackle these shared references, please let me know.