如何在迭代集合时修改该集合?

8
我有一个 “Board”(即“&mut Vec>”)需要在迭代时更新。我希望用函数生成的新值来更新它,这个函数需要一个 “&Vec>” 作为其参数。
我尝试了几种方法:
1. 使用 “board.iter_mut().enumerate()” 和 “row.iter_mut().enumerate()”,以便我可以在最内部的循环中更新 “cell”。但是 Rust 不允许调用 “next_gen” 函数,因为它需要一个 “&Vec>”,而当你已经拥有一个可变引用时就不能再有不可变引用了。 2. 修改 “next_gen” 函数签名以接受一个 “&mut Vec>”。但是 Rust 不允许对一个对象进行多个可变引用。
我目前将所有的更新推迟到一个 “HashMap” 中,然后在执行迭代后应用它们:
fn step(board: &mut Board) {
    let mut cells_to_update: HashMap<(usize, usize), Cell> = HashMap::new();
    for (row_index, row) in board.iter().enumerate() {
        for (column_index, cell) in row.iter().enumerate() {
            let cell_next = next_gen((row_index, column_index), &board);
            if *cell != cell_next {
                cells_to_update.insert((row_index, column_index), cell_next);
            }
        }
    }

    println!("To Update: {:?}", cells_to_update);
    for ((row_index, column_index), cell) in cells_to_update {
        board[row_index][column_index] = cell;
    }
}

完整源代码

是否有一种方法可以使这段代码在最内层循环中“原地”更新board,同时仍然能够在最内层循环中调用next_gen?

免责声明:

我正在学习Rust,并且我知道这不是最好的做法。我在尝试了解我能够做什么和不能做什么。我还试图限制任何复制以稍微限制自己。正如oli_obk - ker提到的那样,这个康威生命游戏的实现是有缺陷的

这段代码旨在评估以下几点:

  1. 这是否可行
  2. 它是否符合惯用的Rust风格

从评论中我所了解的是,使用std::cell::Cell是可能的。然而,使用std:cell:Cell绕过了一些核心的Rust原则,这也是我在原问题中描述的“困境”。

2个回答

6

有没有一种方法可以使这段代码“原地”更新面板?

存在一种专门用于此类情况的类型。它巧合地被称为std::cell::Cell。即使多次不可变地借用,您也可以改变Cell的内容。 Cell仅限于实现了Copy的类型(对于其他类型,您必须使用RefCell,如果涉及多个线程,则必须结合Mutex等东西使用Arc)。

use std::cell::Cell;

fn main() {
    let board = vec![Cell::new(0), Cell::new(1), Cell::new(2)];

    for a in board.iter() {
        for b in board.iter() {
            a.set(a.get() + b.get());
        }
    }
    println!("{:?}", board);
}

正如文档所述,“内部可变性是最后的手段”,我相信在回退到单元格类型之前还有其他探索途径。 - oli_obk
我不明白为什么使用Cell会成为最后的选择,因为它没有运行时成本。使用不同方法或使用Cell之间的选择取决于个人偏好。 - A.B.
2
问题不在于运行时成本,而在于规避 Rust 的借用检查规则,这些规则不仅有助于防止内存不安全,还有助于防止在任意位置修改相同内存位置而导致的意外行为,而不仅仅是在您拥有 &mut 引用的位置。可以将其类比于 C++ 类成员上的 mutable 关键字。您的非可变对象突然被允许进行修改。 - oli_obk
我同意单元格不应该公开暴露给用户,但作为抽象的构建块是完全可以的。 - A.B.
  1. 它允许多次写入的逻辑错误,这可能是不需要的。
  2. 它只是近似为零成本,在现实中它可能不会/不能像预期的那样进行优化。当然,这取决于llvm和rustc的变化。
- bluss
目前,这是唯一回答了我的原始问题。虽然在这里已经讨论过,它可能不是最安全的解决方案,但它是解决问题的一种方法。 - arnm

5

这完全取决于你的next_gen函数。假设我们除了函数签名以外对该函数一无所知,最简单的方法是使用索引:

fn step(board: &mut Board) {
    for row_index in 0..board.len() {
        for column_index in 0..board[row_index].len() {
            let cell_next = next_gen((row_index, column_index), &board);
            if board[row_index][column_index] != cell_next {
                board[row_index][column_index] = cell_next;
            }
        }
    }
}

关于next_gen的更多信息可能会有不同的解决方案,但对我来说它听起来很像一个元胞自动机。据我所知,在Rust中,如果不改变Board的类型,这不能以迭代器方式完成。

您可能担心索引解决方案比迭代器解决方案效率低,但是您应该相信LLVM在这方面的表现。如果您的next_gen函数在另一个crate中,您应该将其标记为#[inline],这样LLVM也可以对其进行优化(如果一切都在一个crate中,则不需要)。


这不是您问题的答案,而是针对您的问题:

由于您正在实现康威生命游戏,因此无法就地进行修改。想象一下以下模式:

00000
00100
00100
00100
00000

如果您更新第二行,它将把该行中的1更改为0,因为它的邻居仅有两个1。这将导致中间的1只看到两个1,而不是最初的三个。因此,您始终需要复制整个Board,或者像您在代码中所做的那样,将所有更改写入其他位置,并在遍历整个棋盘后将它们插入其中。

是的,这将是最简单的解决方案,但我只是想看看是否有其他使用迭代器的方法。顺便说一下,代码在这里 - arnm
除非您创建自己的迭代器(并且可能是特定于您的单元格更新函数的板类型),否则使用迭代器是不可能的。另外,除非您的更新函数仅访问当前单元格右侧和底部的单元格,否则您将获得非常奇怪的行为,并且绝对不会是同步的元胞自动机。用这种方式实现生命游戏是不可能的。 - oli_obk
是的,那肯定会成为一个问题。谢谢你提醒我。我并没有太专注于实现康威生命游戏的实际问题,而是一直在尝试学习 Rust。 - arnm

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