在 Rust 的 ndarray 中改变移动窗口

6

我正在尝试使用 ndarray 库在 Rust 中实现康威生命游戏 (Conway's Game of Life) 的一个迭代。我认为可以通过循环遍历数组的 3x3 窗口来简单地计算活着的邻居数量,但是我在进行实际更新时遇到了问题。

数组中用 # 表示生命,用 表示没有生命:

let mut world = Array2::<String>::from_elem((10, 10), " ".to_string());
for mut window in world.windows((3, 3)) {
    let count_all  = window.fold(0, |count, cell| if cell == "#" { count + 1 } else { count });
    let count_neighbours = count_all - if window[(1, 1)] == "#" { 1 } else { 0 };
    match count_neighbours {
        0 | 1   => window[(1, 1)] = " ".to_string(), // Under-population
        2       => {},                               // Live if alive
        3       => window[(1, 1)] = "#".to_string(), // Re-produce
        _       => window[(1, 1)] = " ".to_string(), // Over-population
    }
}

这段代码无法编译!报错信息在match块内,出现了"error: cannot borrow as mutable"和"error: cannot assign to immutable index"。我尝试过使用for &mut window...,但是库貌似没有实现该方法(?)。
我对Rust相对陌生,我认为这可能与库提供的窗口实现有关。但我不确定,也不知道是否有某种变化/修复可以让我继续使用这种方式。我需要完全舍弃这种方式吗?我不确定在这种情况下最好的方法是什么。
非常欢迎任何其他建议或代码改进。
(这段代码没有实现正确的规则,因为我正在循环中进行变更并忽略了外部边缘,但在这种情况下这是可以接受的。而且,任何这样做的变化都可以——细节并不重要。)

2
有趣的是,您对康威生命游戏的处理方式可能不正确,因为在计算邻居单元格时,您正在逐步改变网格。该游戏假定所有邻居检查都是在每个帧中一次性执行的,在任何细胞死亡或繁殖之前。因此,将游戏状态复制到第二个对象中可以避免您问题中的问题,并且通常会更好地运作。 - E net4
1
我得说我没有按照正确的规则实现,而且这也没关系——主要原因是我很难制作一个适用于副本的版本,因为我不能使用另一种循环方式来处理“windows”! - QasimK
1
那听起来像是一个很好的挑战,你可以去研究一下。 - E net4
1个回答

1
您使用ndarraywindows的一般方法是正确的,但问题在于您从windows迭代器中获取的值将始终是不可变的。您可以通过将值包装在CellRefCell中来解决这个问题,这会给您内部可变性。也就是说,它们将一个值包装为不可变的,但提供了一个API让您无论如何都可以对其进行改变。
以下是您的代码,已经相当残酷地适应了RefCell的使用:
use ndarray::Array2;
use std::cell::RefCell;

fn main() {
    // creating variables for convenience, so they can be &-referenced
    let alive = String::from("#");
    let dead = String::from(" ");

    let world = Array2::<String>::from_elem((10, 10), " ".to_string());
    let world = world.map(RefCell::new);

    for mut window in world.windows((3, 3)) {
        let count_all  = window.fold(0, |count, cell| if *cell.borrow() == &alive { count + 1 } else { count });
        let count_neighbours = count_all - if *window[(1, 1)].borrow() == &alive { 1 } else { 0 };
        match count_neighbours {
            0 | 1   => *window[(1, 1)].borrow_mut() = &dead,  // Under-population
            2       => {},                                    // Live if alive
            3       => *window[(1, 1)].borrow_mut() = &alive, // Re-produce
            _       => *window[(1, 1)].borrow_mut() = &alive, // Over-population
        }
    }
}

我所做的只是让你的代码能够正常工作,几乎没有改动。但是,正如E_net4指出的那样,你的解决方案存在一个重大缺陷,因为它在读取时进行了变异。此外,在最佳实践方面,你使用String并不理想。一个enum更好,因为它更小,可以堆栈分配,并更好地捕获模型的不变性。通过enum,您将衍生以下Copy,这将让您使用Cell而不是RefCell,这很可能会有更好的性能,因为它复制数据,而不是必须计算引用次数。
#[derive(Debug, PartialEq, Clone, Copy)]
enum CellState {
    Alive,
    Dead
}

更正:使用 Cell 没有运行时开销(因为它只是复制底层值),但是使用 RefCell 会有轻微的运行时开销(它像读写互斥锁一样进行引用计数)。 - Matthieu M.

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