在 Rust 中处理大型可变结构的最佳方法是什么?

What is the best way of dealing with a big mutable struct in Rust?

提问人:oui 提问时间:9/7/2023 最后编辑:ShadowRangeroui 更新时间:9/7/2023 访问量:77

问:

这是我的问题的简化视图:

struct Game {
    currentPlayer: u8,
    players: Vec<Player>,
    units: Vec<Unit>,
}

fn update(game: &mut Game) {
    for unit in game.units.iter_mut().filter(|u| u.owner == game.currentPlayer) {
        if unit.repairPrice < game.getPlayer(game.currentPlayer).money {
            unit.health = 100;
            game.getPlayer(game.currentPlayer).money -= unit.repairPrice;
        }
    }
}

impl Game {
    pub fn getPlayer(&mut self, player: u8) -> &mut Player {
        return self.players.iter_mut().find(|p| p.color == player).unwrap();
    }
}

在这里,编译器说我不能借用值 currentPlayer 作为不可变的,因为 game 已经被借用为可变的。

虽然我理解编译器在说什么以及为什么这么说,但我不知道我应该改变什么。单位和玩家都必须同时更新,所以似乎无论我怎么做,它总是会引起冲突。

我可以克隆整个结构,对其进行迭代和计算,然后在原始可变结构中找到相应的单位和玩家来更新它们,但效率非常低。

借用检查器 所有权

评论

0赞 eggyal 9/7/2023
在这种情况下,您可以在进入循环之前将当前玩家分配给局部变量:然后使用它来代替调用循环中的 getter。let mut player = game.getPlayer(game.currentPlayer);
0赞 ShadowRanger 9/7/2023
@eggyal:很确定这取决于 的定义,他们没有提供;似乎本身就可能导致借用冲突,这是在可变迭代结束和基于涉及之前。真的,这需要一个最小的可重现的例子,而且这并不完整,在不知道确切的所有权语义的情况下,有太多可能的陷阱。getPlayergame.getPlayer(game.currentPlayer)game.unitsfiltergame.currentPlayer
0赞 oui 9/7/2023
@eggyal 这是行不通的,你只是在借用循环使用单位时出现错误。
0赞 oui 9/7/2023
@ShadowRanger getPlayer 只是从 players 向量返回一个 player。我进行了编辑以将函数包含在问题中,谢谢。
1赞 Chayim Friedman 9/7/2023
请发布最小可重复示例

答:

1赞 ShadowRanger 9/7/2023 #1

将您的代码扩展为针对您的问题的重现器后,很明显,如果没有以下功能,您就无法完成这项工作:

  1. 实质性的重构,
  2. 很多浪费的克隆和/或东西,或者unsafe
  3. 内联 into 代码(冗余、可怕)getPlayerupdate

根本问题是,当一个函数说它可变地借用一个对象时,它借用了整个对象。因此,即使你知道它只触及属性,Rust 的借用检查器(它适用于函数接口,而不是内容)也必须假设它可以对它收到的引用做任何事情。getPlayerplayersGame

我能想到的最小的解决方案是使它成为一个自由函数,它只接受对对象的可变引用,因此可以以一种明确的方式进行分析,使其既不会被多次借用。getPlayerVecPlayerupdateVec

fn update(game: &mut Game) {
    for unit in game.units.iter_mut().filter(|u| u.owner == game.currentPlayer) {
        // Passed mutable reference to the players only, rather than being called on a whole Game
        if unit.repairPrice < getPlayer(&mut game.players, game.currentPlayer).money {
            unit.health = 100;
            // Same change as above
            getPlayer(&mut game.players, game.currentPlayer).money -= unit.repairPrice;
        }
    }
}

// Made a free function so it can accept the Vec alone, not the whole game
fn getPlayer(players: &mut Vec<Player>, player: u8) -> &mut Player {
    return players.iter_mut().find(|p| p.color == player).unwrap();
}

这很好用(尽管它确实抱怨您的非惯用变量名称; FTW,这些废话都不是)。snake_casecamelCase

或者,getPlayer 内联到 update 中;它不需要太多余,因为您可以缓存结果并且只复制一次代码,而不是两次(作为奖励,这可能意味着只做一次工作,而不是每个与该播放器关联的单位两次);根本不需要,您只需调整:getPlayerupdate

fn update(game: &mut Game) {
    let player = game.players.iter_mut().find(|p| p.color == game.currentPlayer).unwrap();
    for unit in game.units.iter_mut().filter(|u| u.owner == game.currentPlayer) {
        if unit.repairPrice < player.money {
            unit.health = 100;
            player.money -= unit.repairPrice;
        }
    }
}

也就是说,我认为这里更好的方法可能是将一个附加到每个 ,而不是打开它,而不是与玩家分开管理单位。这样一来,就不会发生冲突,因为你只需可变地获取单个对象,并对附加到它的 s 进行操作(不触及 的任何其他元素,并且操作效率更高,因为您不需要继续查找玩家,不需要处理每个单元,只需要处理该玩家的单元等)。如果你需要把所有的 s 作为迭代器,一个简单的方法可以让你迭代玩家,并将他们的单位组合成一个逻辑迭代器。Vec<Unit>PlayerGamegetPlayerupdatePlayerunitGameUnitflat_map

评论

0赞 oui 9/7/2023
借用检查器对 getPlayer 有问题,但对内联它没有问题,这似乎很奇怪。有没有办法告诉编译器“内联此函数”,这样就不需要复制粘贴代码了?将单位放入 Player 结构体并不能解决我无法同时编辑结构体的 2 个变量的基本问题,而且我会一次又一次地遇到同样的问题。这似乎是一个非常大的限制,如果它阻止了编写如此简单的代码。
0赞 ShadowRanger 9/7/2023
@oui:为了避免无限的递归复杂性问题,Rust 借用检查器在任何给定点(简化一点)一次查看一个范围。任何离开该范围的调用都只查看被调用函数的原型(参数类型、返回值以及与之关联的任何生存期注释),因此,如果原型不准确或过于宽泛,借用检查器不会注意到,它只会关注接口声称要执行的操作。如果你说你借用了一个可变的引用,你可以改变它中的任何一个Game
0赞 ShadowRanger 9/7/2023
这有时是有帮助的;如果你的函数最终需要改变更多的 ,则可以在不更改原型的情况下这样做;通过借用检查器的工作代码不能开始失败借用检查,因为它调用的函数之一的实现发生了更改。但在这种情况下,这也使它变得痛苦。内联有效,因为现在借用检查器可以看到所有内容,但作为一个单独的函数,它只能知道你做了一个“可变借用”和一个可变借用(可以合法地改变)并抱怨。GameGamegame.unitsgamegame.units