Since in your scenario you need to simultaneously have a mutable reference and an immutable reference on two elements in the same container, I think you need the help of interior mutability.
This will check at run-time that the same element is not accessed simultaneously through a mutable (.borrow_mut()
) and immutable (.borrow()
) reference (otherwise it panics).
Obviously, you have to ensure that by yourself (which is quite ugly since we have to compare pointers!).
Apparently, it is necessary to reach pointers because references cannot be compared (the self
argument of std::cmp::PartialEq::eq() will be dereferenced). The documentation of std::ptr::eq()
(which should probably be used here) shows the difference between comparing references and comparing pointers.
struct Combatant {
current_health: i16,
max_health: i16,
}
struct Battlefield {
combatants: Vec<std::cell::RefCell<Combatant>>,
}
impl Combatant {
fn attack(
&self,
other: &mut Combatant,
) {
other.current_health -= 3;
}
}
impl Battlefield {
fn tick(&mut self) {
let target_cell = &self.combatants[0];
let target = &*target_cell.borrow();
for combatant_cell in &self.combatants {
let combatant = &*combatant_cell.borrow();
// if combatant as *const _ != target as *const _ {
if !std::ptr::eq(combatant, target) {
let target_mut = &mut *target_cell.borrow_mut();
combatant.attack(target_mut);
}
}
}
}
Note that this interior mutability was bothering me at first, and seemed like « bending the rules » because I was reasoning essentially in terms of « immutable becoming suddenly mutable » (like const-casting in C++), and the shared/exclusive aspect was only the consequence of that.
But the answer to the linked question explains that we should think at first in terms of shared/exclusive access to our data, and then the immutable/mutable aspect is just the consequence.
Back to your example, the shared access to the Combatant
s in the vector seems essential because, at any time, any of them could access any other.
Because the consequence of this choice (shared aspect) is that mutable accesses become almost impossible, we need the help of interior mutability.
This is not « bending the rules » because strict checking is done on .borrow()
/.borrow_mut()
(small overhead at this moment) and the obtained Ref
/RefMut
have lifetimes allowing usual (static) borrow-checking in the portion of code where they appear.
It is much safer than free immutable/mutable accesses we could perform with other programming languages.
For example, even in C++ where we could consider target
as const (reference/pointer to const
) while iterating on the non-const combatants
vector, one iteration can accidentally mutate the target
that we consider as const (reference/pointer to const
means « I won't mutate it », not « it cannot be mutated by anyone »), which could be misleading. And with other languages where const
/mut
do not even exist, anything can be mutated at any time (except for objects which are strictly immutable, like str
in Python, but it becomes difficult to manage objects with states that could change over time, like current_health
in your example).