如何方便地将二维数组转换为二维向量?



我正在遵循 Rust-wasm 教程,我希望能够在生命游戏中轻松地将一艘船(实际上是形状(添加到宇宙中。

作为第一步,我想将表示形状的01的二维数组转换为表示宇宙中形状坐标的索引向量。

我有一段工作代码,但我想让它更加用户友好:

const WIDTH: u32 = 64;
const HEIGHT: u32 = 64;
/// glider: [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    let mut ship: Vec<u32> = Vec::new();
    for row_idx in 0..shape.len() {
        for col_idx in 0..shape[row_idx].len() {
            let cell = shape[row_idx][col_idx];
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }
    ship
}
#[test]
fn glider() {
    let glider  = vec![vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
    println!("{:?}", make_ship(glider));
}

test显示了我的问题:vec! s的冗长,理想情况下,我希望能够在没有所有vec!的情况下编写它。make_ship代码不应关心形状数组的大小。理想示例:

let glider = [[0, 1, 0],
              [0, 0, 1],
              [1, 1, 1],];

问题是:如何用简单的数组很好地表达形状,并具有make_ship任意大小的二维向量的功能?

使用自定义宏可以减少 vec! s 的数量:

#[macro_export]
macro_rules! vec2d {
    ($($i:expr),+) => { // handle numbers
        {
            let mut ret = Vec::new();
            $(ret.push($i);)*
            ret
        }
    };
    ([$($arr:tt),+]) => { // handle sets
        {
            let mut ret = Vec::new();
            $(ret.push(vec!($arr));)*
            ret
        }
    };
}
fn main() {
    let glider = vec2d![[0, 1, 0],
                        [0, 0, 1],
                        [1, 1, 1]];
    let glider2 = vec2d![[0, 1, 0, 1],
                         [0, 0, 1, 0],
                         [1, 1, 1, 0],
                         [0, 1, 1, 0]];

    println!("{:?}", glider);  // [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
    println!("{:?}", glider2); // [[0, 1, 0, 1], [0, 0, 1, 0], [1, 1, 1, 0], [0, 1, 1, 0]]
}

在 Rust 迭代器的帮助下,您的初始函数也可以进行一些改进:

fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    shape
        .iter()
        .enumerate()
        .flat_map(|(row, v)| {
            v.iter().enumerate().filter_map(move |(col, x)| {
                if *x == 1 {
                    Some(col as u32 + row as u32 * WIDTH)
                } else {
                    None
                }
            })
        })
        .collect()
}

Vec<Vec<_>>实际上不是一个二维向量,而是一个"向量的向量"。这具有重大含义(假设外部向量被解释为行,内部向量被解释为列(:

  1. 行可以有不同的长度。这通常不是你想要的。
  2. 行是单个对象,可能分散在整个堆内存中。
  3. 为了访问元素,您必须遵循两个间接寻址。

我会实现一个二维向量,而不是一个一维向量,并提供有关其维度的附加信息。像这样:

struct Vec2D<T> {
    n_rows: usize,  // number of rows
    n_cols: usize,  // number of columns (redundant, since we know the length of data)
    data: Vec<T>,   // data stored in a contiguous 1D array
}

此结构可以初始化

let glider = Vec2D {
    n_rows: 3,
    n_cols: 3,
    data: vec![0, 1, 0, 
               0, 0, 1, 
               1, 1, 1],
};

或者更方便地使用采用数组的数组的函数或宏。(参见@ljedrz的答案以获取灵感(。

要访问结构中的元素,您必须使用一些数学知识将 2D 索引转换为 1D 索引:

impl<T> Vec2D<T> {
    fn get(&self, row: usize, col: usize) -> &T {
         assert!(row < self.n_rows);
         assert!(col < self.n_cols);
         &self.data[row * self.n_cols + col]
    }
}

虽然实现自己的二维数组类型是一项有趣的练习,但为了高效使用,使用现有解决方案(如 ndarray crate(可能更有效。

另一种解决方案是使用 AsRef 透明地处理Vec<T>[T]

fn make_ship<T>(shape: &[T]) -> Vec<u32>
where
    T: AsRef<[u32]>,
{
    let mut ship: Vec<u32> = Vec::new();
    for row_idx in 0..shape.len() {
        let row = shape[row_idx].as_ref();
        for col_idx in 0..row.len() {
            let cell = row[col_idx];
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }
    ship
}

这将处理以下内容:

let glider = vec![vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
let glider = [[0, 1, 0], [0, 0, 1], [1, 1, 1]];
let glider = [vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
let glider = vec![[0, 1, 0], [0, 0, 1], [1, 1, 1]];

更好的解决方案是根本不关心切片/向量,并使用迭代器:

fn make_ship<'a, T, U>(shape: &'a T) -> Vec<u32>
where
    &'a T: std::iter::IntoIterator<Item = U>,
    U: std::iter::IntoIterator<Item = &'a u32>,
{
    let mut ship: Vec<u32> = Vec::new();
    for (row_idx, row) in shape.into_iter().enumerate() {
        for (col_idx, &cell) in row.into_iter().enumerate() {
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }
    ship
}

它也可以处理上述情况,但如果它提供了这样的迭代器,也可以处理诸如 @kazemakase Vec2D之类的类型。