使用结构体满足Rust借用检查器

3
我正在尝试学习 Rust,正如你所想象的那样,借用检查器是我最大的对手。这是我的设置,它是一种用于游戏战舰的板条箱。游戏基于 Battlefield 结构体,该结构体由 Cell 组成。Cell 可以引用 Ship,而 Ship 具有所有引用它的 Cell 的向量,因此它是双向只读关系。
pub struct Battlefield<'a> {
    cells: Vec<Vec<Cell<'a>>>,
}

#[derive(Debug, PartialEq)]
pub struct Cell<'a> {
    ship: Option<&'a Ship<'a>>
}

#[derive(Debug, PartialEq)]
pub struct Ship<'a> {
    length: usize,
    cells: Vec<&'a Cell<'a>>,
}

我的问题是Battlefieldplace_ship函数:

impl<'a> Battlefield<'a> {
    pub fn place_ship(&mut self,
                      ship: &'a mut Ship,
                      x: usize,
                      y: usize,
                      orientation: Orientation)
                      -> PlaceResult {
        // check ship placement in bounds
        // check affected cells are free
        // set cells' ship ref to ship
        // add cell refs to ship's cells field
    }
}

我觉得这很有道理,而且我认为这里没有所有权问题,但是似乎我错了:

#[cfg(test)]
mod tests {
    use super::{Battlefield, X, Y};
    use super::Orientation::*;
    use super::super::ship::Ship;

    #[test]
    fn assert_ship_placement_only_in_bounds() {
        let mut ship = Ship::new(3);
        let mut bf = Battlefield::new();

        assert_eq!(Ok(()), bf.place_ship(&mut ship, 0, 0, Horizontal));
        assert_eq!(Ok(()), bf.place_ship(&mut ship, 5, 5, Vertical));
    }
}

src/battlefield.rs:166:47: 166:51 error: cannot borrow `ship` as mutable more than once at a time [E0499]
src/battlefield.rs:166         assert_eq!(Ok(()), bf.place_ship(&mut ship, 5, 5, Vertical));
                                                                 ^~~~
src/battlefield.rs:165:47: 165:51 note: first mutable borrow occurs here
src/battlefield.rs:165         assert_eq!(Ok(()), bf.place_ship(&mut ship, 0, 0, Horizontal));
                                                                 ^~~~

我知道这只是一个简短的摘录,但整个代码太长了,无法在此处发布。可以在这里找到该项目(使用'cargo build'进行标准构建)。

1
但是整个代码太长了,无法在此处发布。我保证你可以将代码缩小到可以在此处发布,同时仍然能够重现相同的错误。请参见[MCVE]。 - Shepmaster
可能是 https://dev59.com/JlwY5IYBdhLWcg3ws5sF 的重复;或者是 https://dev59.com/0mIj5IYBdhLWcg3wCg_X 或者 https://dev59.com/jIjca4cB1Zd3GeqPv2Po 或者 https://dev59.com/kYrda4cB1Zd3GeqPJC9q。或者是关于可变别名的任何问题。正如错误所述,你不能同时多次借用**任何东西**作为可变。 - Shepmaster
3
"bi-directional" - 双向的 "I don't think there's an ownership problem" - 我不认为存在所有权问题 There's always an ownership problem once you've got bidirectional relationships. - 一旦你有了双向关系,就总会存在所有权问题。 - Sebastian Redl
@SebastianRedl 感谢您的建设性评论。很高兴您记得在引用中省略了“只读”!虽然我在处理 Rust 时可能是个新手,但非可变引用与所有权无关。 - Leopard2A5
2
@Leopard2A5 参考,无论是 mut 还是 non-mut,对所有权的影响都是一样的:所有者必须保持对象存活,直到引用消失。Mut 或 non-mut 只是影响可以采取哪些其他引用。是的,mut 引用由于别名问题使双向关系更加困难,但 Rust 使任何类型的双向关系都很困难。只需看看实现双向链表有多么棘手就知道了。归根结底,在 Rust 中,重新设计直到双向关系消失往往是最好的选择。 - Sebastian Redl
@SebastianRedl,为什么不一开始就像这样向我解释呢?为什么要让你的第一条评论听起来很傲慢呢?无论如何,感谢您的解释。 - Leopard2A5
1个回答

3
Battlefield::place_ship 的签名来看,编译器必须认为该函数可能会在 self(即 Battlefield<'a> 对象)中存储对 ship 的可变引用。这是因为你将 ship 参数的生命周期与 Battlefield 的生命周期参数进行了链接,并且编译器只查看结构体的高级接口,以便所有外观相同的结构体都具有相同的行为(否则,即使所有字段都是私有的,向结构体添加一个字段也可能是破坏性的更改!)。
如果你将 ship 的声明从 ship: &'a mut Ship 更改为 ship: &mut Ship<'a>,你会发现错误消失了(如果方法体不使用该参数)。但是,如果你尝试将此指针的副本存储在 Cellship 字段中,则不再起作用,因为现在编译器无法证明 Ship 将活得足够长。
你将继续遇到有关生命周期的问题,因为你尝试做的事情不能使用简单的引用来完成。现在,在定义 BattlefieldCellShip 时存在矛盾:你声明 Battlefield 拥有引用 ShipCell,而这些引用的生命周期将超过 Battlefield。但是,同时,你又声明 Ship 引用的 Cell 将超过 Ship。唯一可行的方法是在同一个 let 语句中声明 BattlefieldShip(因为编译器将为所有值分配相同的生命周期)。
let (mut ship, mut bf) = (Ship::new(3), Battlefield::new());

你还需要将&mut self更改为&'a mut self,以便从self分配一个CellShip。但是,一旦调用place_ship,你就会有效地锁定Battlefield,因为编译器将认为Battlefield可以存储对自身的可变引用(因为它将可变引用作为参数传递给自身!)。
更好的方法是使用引用计数代替简单引用,结合内部可变性代替显式可变性。引用计数意味着你不必处理生命周期(尽管在这里,你必须使用弱指针打破循环以避免内存泄漏)。内部可变性意味着你可以传递不可变引用而不是可变引用;这将避免cannot borrow x as mutable more than once编译器错误,因为根本没有可变借用。

1
在我看来,“更好”的(但结构不同)解决方案是不要让子组件引用父结构,而是仅在特定子方法需要时传递父引用。 - Shepmaster
@Shepmaster 我想存储船只中单元格的引用,以便轻松查找船只的所有单元格是否已被击中。但你可以看出我来自一个垃圾回收的背景 :) - Leopard2A5
谢谢你的回答,Francis!我不能说我第一次阅读就完全理解了它 :) 我看到我仍然有很多关于 Rust 的东西需要学习。 - Leopard2A5
@Leopard2A5 如果你有一个方法 impl Ship { fn all_were_hit(&self) -> bool {} },尝试将其更改为 impl Ship { fn all_were_hit(&self, &Battlefield) -> bool {} }(或适当的类型),看看效果如何。 - Shepmaster

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