I'm trying to write Tetris in rust. I have structs in this project that I want to treat as immutable even though they do mutate.
The approach I'm using to achieve this kind of behavior is this:
#[derive(Debug)]
struct Example {
foo: i8
}
impl Example {
fn change(mut self) -> Self {
self.foo = 8;
self
}
}
which allows you to do stuff like this:
let first = Example { foo: 0 };
let second = first.change();
println!("{:?}", second); // Example { foo: 8 }
but yells at you when you do things like this:
let first = Example { foo: 0 };
let second = first.change();
println!("{:?}", first); // error[E0382]: borrow of moved value: `first`
The part where I'm confused is, why does this work:
#[derive(Debug)]
struct Matrix {
cells: [[char; 2]; 2]
}
impl Matrix {
fn new() -> Self {
Matrix {
cells: [['░'; 2]; 2]
}
}
fn solidify(mut self, row: usize, column: usize) -> Self {
self.cells[row][column] = '█';
self
}
}
fn main() {
let matrix = Matrix::new();
let matrix = matrix.solidify(0, 0);
println!("{:?}", matrix); // Matrix { cells: [['█', '░'], ['░', '░']] }
}
when this doesn't?
#[derive(Debug)]
struct Matrix {
cells: [[char; 2]; 2]
}
impl Matrix {
fn new() -> Self {
Matrix {
cells: [['░'; 2]; 2]
}
}
fn solidify(mut self, row: usize, column: usize) -> Self {
self.cells[row][column] = '█';
self
}
}
#[derive(Debug)]
struct Tetris {
matrix: Matrix
}
impl Tetris {
fn new() -> Self {
Tetris {
matrix: Matrix::new()
}
}
fn change(&mut self) {
self.matrix = self.matrix.solidify(0, 0);
/* -----------------------------------------
This is where it yells at me ^ */
}
}
fn main() {
let mut tetris = Tetris::new();
tetris.change();
println!("{:?}", tetris); // error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
}
This gives:
error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
--> src/main.rs:32:23
|
32 | self.matrix = self.matrix.solidify(0, 0);
| ^^^^^^^^^^^ -------------- `self.matrix` moved due to this method call
| |
| move occurs because `self.matrix` has type `Matrix`, which does not implement the `Copy` trait
|
note: `Matrix::solidify` takes ownership of the receiver `self`, which moves `self.matrix`
--> src/main.rs:13:21
|
13 | fn solidify(mut self, row: usize, column: usize) -> Self {
| ^^^^
I've done some research and I feel like either std::mem::swap, std::mem::take, or std::mem::replace,
could do the trick for me, but I'm not exactly sure how.
You're right. mem::[take,replace]()
can do the work.
The problem is that while you can leave a variable uninitialized for a time, you cannot leave a mutable reference uninitialized for a time (by moving out of it), even if you reassign it after.
There is a reason to this limitation: panics. If matrix.solidify()
panics, we will exit without executing the recurring assignment to matrix
. Later, we could recover from the panic, and observe the moved-from matrix
.
Without dependencies (and unsafe code), the only solution is to leave something behind even when we reassign, so that even if we panic matrix
stays initialized. std::mem::take()
can help with that if Matrix
implements Default
- it leaves the default value, while the more general std::mem::replace()
can help otherwise - it leaves some value:
#[derive(Debug, Default)]
struct Matrix {
cells: [[char; 2]; 2]
}
impl Tetris {
fn change(&mut self) {
let matrix = std::mem::take(&mut self.matrix);
self.matrix = matrix.solidify(0, 0);
}
}
Or:
#[derive(Debug)] // No `Default`.
struct Matrix {
cells: [[char; 2]; 2]
}
impl Tetris {
fn change(&mut self) {
let matrix = std::mem::replace(&mut self.matrix, Matrix::new());
self.matrix = matrix.solidify(0, 0);
}
}
If this is not good enough for you (for example, because you don't have a good default value to insert, or because of performance requirements) you can use the replace_with
crate. It provides replace_with::replace_with_or_abort()
, that will just abort the whole process in case of a panic, preventing the possibility of recovering:
impl Tetris {
fn change(&mut self) {
replace_with::replace_with_or_abort(&mut self.matrix, |matrix| matrix.solidify(0, 0));
}
}
Note that instead of what you're doing now, you may actually want interior mutability.