【问题标题】:How can I modify a collection while also iterating over it?如何在迭代集合的同时修改集合?
【发布时间】:2015-09-07 03:10:30
【问题描述】:

我有一个Board(又名&mut Vec<Vec<Cell>>),我想在迭代它时对其进行更新。我要更新的新值来自一个函数,该函数需要 &Vec<Vec<Cell>> 到我正在更新的集合。

我已经尝试了几件事:

  1. 使用board.iter_mut().enumerate()row.iter_mut().enumerate() 以便我可以更新最内层循环中的cell。 Rust 不允许调用 next_gen 函数,因为它需要 &Vec<Vec<Cell>> 并且当你已经有一个可变引用时你不能有一个不可变引用。

  2. next_gen 函数签名更改为接受&mut Vec<Vec<Cell>>。 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;
    }
}

Full source

有没有办法让这段代码“就地”更新board,即在最内层循环内,同时仍然能够在最内层循环内调用next_gen

免责声明

我正在学习 Rust,我知道这不是最好的方法。我在玩,看看我能做什么,不能做什么。我也试图限制任何复制以限制自己一点。作为oli_obk - ker mentions, this implementation for Conway's Game of Life is flawed

这段代码旨在衡量几件事:

  1. 如果这是可能的
  2. 如果是惯用的 Rust

根据我在 cmets 中收集到的信息,std::cell::Cell 是可能的。但是,使用 std:cell:Cell 会绕过一些核心 Rust 原则,我在原始问题中将其描述为我的“困境”。

【问题讨论】:

    标签: collections iterator rust immutability


    【解决方案1】:

    有没有办法让这段代码“就地”更新板子?

    有一种专门为此类情况而设计的类型。巧合的是,它被称为std::cell::Cell。您可以更改 Cell 的内容,即使它已被多次不可变地借用。 Cell 仅限于实现Copy 的类型(对于其他类型,您必须使用RefCell,如果涉及多个线程,那么您必须将ArcMutex 之类的东西结合使用)。

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

    【讨论】:

    • 如文档所述,内部可变性是最后的手段,我相信在回归细胞类型之前还有其他途径可以探索
    • 我看不到 Cell 在没有运行时成本的情况下如何成为最后的手段。使用不同方法或使用 Cell 之间的选择取决于偏好。
    • 问题不是运行时成本,而是对 Rust 借用检查规则的规避,这不仅有助于防止内存不安全,而且还可以防止因修改相同内存而导致的意外行为位置在任意位置,而不是仅仅在您有 &amp;mut 参考的位置。想想它有点像 C++ 类成员上的 mutable 关键字。你的非可变对象突然被允许被修改。
    • 我同意 Cell 不应该公开暴露给用户 - 但作为抽象的构建块它非常好。
    • 1) 它允许可能不需要的多次写入的逻辑错误 2) 近似值只有零成本,实际上它不会/可能不会优化。当然,取决于 llvm 和 rustc 的变化。
    【解决方案2】:

    这完全取决于您的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 的信息,可能会有不同的解决方案,但对我来说这听起来很像元胞自动机,据我所知,如果不更改Board 的类型。

    您可能担心索引解决方案的效率会低于迭代器解决方案,但您应该相信 LLVM。如果您的 next_gen 函数在另一个 crate 中,您应该将其标记为 #[inline] 以便 LLVM 也可以对其进行优化(如果所有内容都在一个 crate 中,则没有必要)。


    不是回答你的问题,而是回答你的问题:

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

    00000
    00100
    00100
    00100
    00000
    

    如果您更新第 2 行,它将将该行中的 1 更改为 0,因为它附近只有两个 1s。这将导致中间的1 只看到两个1s,而不是一开始的三个。因此,您总是需要复制整个Board,或者像您在代码中所做的那样,将所有更改写入其他位置,然后在遍历整个板后将它们拼接起来。

    【讨论】:

    • 是的,这将是最简单的解决方案,但我只是想看看使用迭代器的任何替代方法。顺便说一句,代码是here
    • 使用迭代器是不可能的,除非您创建自己的迭代器(并且可能是板类型),该迭代器专门针对您的单元更新功能量身定制。在相关说明中,除非您的更新功能仅访问当前单元格右侧和底部的单元格,否则您将获得非常奇怪的行为,并且绝对不是同步元胞自动机。生活游戏将无法以这种方式实现。
    • 是的,那肯定是个问题。感谢那。我并没有过多关注实现康威生命游戏的实际问题,而是尝试学习 Rust。
    猜你喜欢
    • 2013-05-13
    • 1970-01-01
    • 1970-01-01
    • 2020-12-20
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    • 2011-11-14
    • 2013-03-04
    相关资源
    最近更新 更多