为什么会出现“无法移动位于可变引用后面的`self.x`”错误?

3

我正在尝试用Rust编写俄罗斯方块游戏。在这个项目中,我有一些结构体,虽然它们会发生变化,但我希望将它们视为不可变的

我采用的方法是:

#[derive(Debug)]
struct Example {
    foo: i8
}

impl Example {
    fn change(mut self) -> Self {
        self.foo = 8;
        self
    }
}

这使您可以做出如下操作:

let first = Example { foo: 0 };
let second = first.change();

println!("{:?}", second); // Example { foo: 8 }

但是当你做这样的事情时,它会对你大喊大叫:

let first = Example { foo: 0 };
let second = first.change();
    
println!("{:?}", first); // error[E0382]: borrow of moved value: `first`

我感到困惑的是,为什么这个代码可以运行:
#[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: [['█', '░'], ['░', '░']] }
}

当这个不是这样的时候?
#[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
}

游乐场

这将会得到:

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 {
   |                     ^^^^

我做了一些研究,感觉std::mem::swap、std::mem::take或std::mem::replace可以帮我解决问题,但我不太确定该怎么做。


1
最有可能的是,你只需要通过引用&mut self来获取self的可变引用,而不是使用mut self,并且在solidifyExample::change中都不返回任何内容。如果你还没有阅读所有权这一章节,我建议你现在就去阅读一下。 - cafce25
当我通过&mut引用获取self时,如何从solidify返回self? 编译器提示错误,显示“期望结构体Matrix,发现&mut Matrix”。 - eliaxelang007
如上所述,“不返回任何内容”。如果您直接对它们进行修改,那么您实际上不必为这些方法返回任何内容。 - cafce25
抱歉,我没有正确阅读你的问题。之前我只是在原地修改它们,但我想让我的代码更加“函数式”,同时仍然保持在原地突变变量而不是一直克隆的性能优势。 - eliaxelang007
2个回答

3

你是对的。mem::[take,replace]()可以完成这项工作。

问题在于,虽然你可以让一个变量未初始化一段时间,但你不能让一个可变引用未初始化一段时间(通过移动它),即使你之后重新分配它

这种限制有其原因:恐慌。如果matrix.solidify()发生恐慌,我们将退出而不执行对matrix的递归赋值。稍后,我们可以从恐慌中恢复,并观察到已经移动的matrix

没有依赖关系(和不安全的代码)的情况下,唯一的解决方案是在重新分配时留下一些东西,以便即使我们恐慌,matrix仍然保持初始化状态。如果Matrix实现了Default,则std::mem::take()可以帮助实现这一点-它会留下默认值,而更通用的std::mem::replace()可以在其他情况下帮助-它会留下某个值:

#[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); 
    } 
}

或者:

#[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); 
    } 
}

如果这不足以满足您的要求(例如,因为您没有一个好的默认值可插入,或者因为性能要求),您可以使用 replace_with crate。它提供了 replace_with::replace_with_or_abort(),在发生 panic 的情况下将会终止整个过程,从而避免恢复的可能性。
impl Tetris {
    fn change(&mut self) {
        replace_with::replace_with_or_abort(&mut self.matrix, |matrix| matrix.solidify(0, 0));
    }
}

请注意,与您现在所做的相比,您可能实际上需要内部可变性

谢谢!我认为 replace_with crate 正是我在寻找的。takereplace 函数本质上是克隆变量以使其工作,而这正是我试图避免的。 - eliaxelang007
对于这种情况,我真的不喜欢 Rust 中的 panic,从 panic 中 "recover" 不应该是可能的。 - Stargateur

1

最近我发现,如果我将同样的拥有可变自身并返回已变异自身模式应用于我的change函数,我实际上不需要replace_with crate。

这整个时间我所要做的就是改变这个:

impl Tetris {
    fn change(&mut self) {
        self.matrix = self.matrix.solidify(0, 0); 
/*      -----------------------------------------
        This is where it yells at me ^                 */
    }
}

转换为:

impl Tetris {
    fn change(mut self) -> Self {
        self.matrix = self.matrix.solidify(0, 0);
/*      -----------------------------------------
        It doesn't yell at me here anymore ^   :D      */
        self
    }
}

然后像这样使用新代码:

fn main() {
    let tetris = Tetris::new();
    let tetris = tetris.change();
    
    println!("{:?}", tetris); // Tetris { matrix: Matrix { cells: [['█', '░'], ['░', '░']] } }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接