使用非复制类型初始化一个大型的固定大小数组

58

我想初始化一个固定大小的数组,其中包含一些可为空、不可复制类型,例如Option<Box<Thing>>这样的一些Thing。我希望将其中两个打包到一个结构体中,而不需要任何额外的间接性。我想写出如下代码:

let array: [Option<Box<Thing>>; SIZE] = [None; SIZE];

但它不起作用,因为[e;n]语法要求e实现Copy。当然,我可以将其扩展为SIZE None,但是当SIZE很大时,这可能会变得笨拙。我不相信可以使用宏来完成这个操作,而不需要对SIZE进行unnatural的编码。有没有一个好方法可以做到这一点?

是的,使用unsafe很容易;但是否有一种不使用unsafe的方法呢?


2
对于[Option<Box<T>>; N],您可以使用从[0; N]转换的transmute:http://is.gd/CC31YQ - oli_obk
我写了一个类似的问题的答案 https://dev59.com/q1oU5IYBdhLWcg3w7qIt - malbarbo
8个回答

40
从Rust 1.63(于2022年8月发布)开始,有一个更简洁的替代方案可用于之前发布的答案,使用std::array::from_fn()函数。
const SIZE: usize = 100;
let array: [Option<Box<Thing>>; SIZE] = std::array::from_fn(|_| None);

游乐场

如果您需要支持较旧的Rust版本,或者需要初始化一个static,可以使用一种替代方法,使用中间的const初始化器,该方法从Rust 1.38(于2019年9月发布)开始可用:

const SIZE: usize = 100;
const INIT: Option<Box<Thing>> = None; // helper
// also works with static array
let array: [Option<Box<Thing>>; SIZE] = [INIT; SIZE]; 

游乐场

这两种方法都可以使用或不使用Box;示例中使用Box是因为问题中使用了它。这两种方法适用于任意大小的数组。

第二种(较旧的)方法的限制是数组项必须具有可以在编译时求值的默认表示 - 常量、枚举变体或由它们组成的原始容器。 None或数字元组可以工作,但非空的VecString则不行。 from_fn()方法没有这样的限制。


29

2
不幸的是,如果SIZE是一个通用参数,那么这种方法就行不通了,因为该参数可能超过32。 - mallwright
@mallwright 没错。请参考答案 https://dev59.com/c14b5IYBdhLWcg3wzUja#66776497,这是一个更近期的解决方案,也适用于通用参数。同时,还可以查看 https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=afe2b75bfffc9851acb7ee2f7135e84d。 - rnstlr
@rnstlr,即使您链接的答案也无法在我的情况下使用通用参数:E0401
无法使用外部函数的通用参数
- Andrew Arnott
1
@AndrewArnott 如果没有代码,很难知道你的问题在哪里。最好在https://play.rust-lang.org上创建一个示例并链接它。 - rnstlr
@rnstlr,是的,我错过了关于关联常量的部分。 - MatrixDev
显示剩余4条评论

28
自Rust 1.55.0(引入了[T]::map())开始,以下内容可用:
const SIZE: usize = 100;

#[derive(Debug)]
struct THING { data: i64 }

let array = [(); SIZE].map(|_| Option::<THING>::default());
for x in array {
    println!("x: {:?}", x);
}

Rust Playground


1
使用map方法需要考虑一些性能问题。文档指出:“不幸的是,目前对该方法的使用并没有得到最佳优化。这主要涉及大型数组,因为在小型数组上进行映射似乎已经得到了很好的优化。此外,请注意,在调试模式下(即没有任何优化),此方法可能会使用大量堆栈空间(数组大小的几倍或更多)。" - Alejandro González
2
@AlejandroGonzález 是指输入或输出数组的大小吗?输入数组只是ZST。 - SOFe
@SOFe [T]::map() 被定义为返回与输入相同大小的数组,因此只需要关注一个大小即可。 - Alejandro González
算了吧,那是指数组的长度!老实说,这是一个我不知道答案的好问题。 - Alejandro González

7

我正在复制chris-morgan的答案,并对其进行修改以更好地适应问题,遵循dbaupp在下文中的建议,并匹配最近的语法更改:

use std::mem;
use std::ptr;

#[derive(Debug)]
struct Thing {
    number: usize,
}

macro_rules! make_array {
    ($n:expr, $constructor:expr) => {{
        let mut items: [_; $n] = mem::uninitialized();
        for (i, place) in items.iter_mut().enumerate() {
            ptr::write(place, $constructor(i));
        }
        items
    }}
}

const SIZE: usize = 50;

fn main() {
    let items = unsafe { make_array!(SIZE, |i| Box::new(Some(Thing { number: i }))) };
    println!("{:?}", &items[..]);
}

注意需要在此处使用unsafe:问题在于,如果构造函数panic!,这将导致未定义的行为。

1
请注意,此解决方案不允许初始化静态(全局)数组。虽然问题没有明确指定是否实际需要,但这可能与未来的读者相关。 - user4815162342

3

遍历堆

如果您可以创建一个包含您类型的 Vec,您可以将其转换为数组:

use std::convert::TryInto;

#[derive(Clone)]
struct Thing;
const SIZE: usize = 100;

fn main() {
    let v: Vec<Option<Thing>> = vec![None; SIZE];
    let v: Box<[Option<Thing>; SIZE]> = match v.into_boxed_slice().try_into() {
        Ok(v) => v,
        Err(_) => unreachable!(),
    };
    let v: [Option<Thing>; SIZE] = *v;
}

在许多情况下,你实际上希望将其保留为 Vec<T>Box<[T]>Box<[T; N]>,因为这些类型都将数据放在堆上。大型数组往往会很大,你不希望所有那些数据都在栈上。
另请参见:

保持简单

打出所有的值:

struct Thing;
const SIZE: usize = 5;

fn main() {
    let array: [Option<Box<Thing>>; SIZE] = [None, None, None, None, None];
}

您可以使用构建脚本为您生成此代码。有关示例,请参见:

5
如果 n = 256,那我是不是真的需要把“None”复制粘贴256遍? - Matej Kormuth
3
取决于很多因素,但我认为这并没有本质上的问题。它很烦人,但是简单易懂。 - Shepmaster
一个这样的因素:如果SIZE是通用的,它确实可以工作。 - mallwright
1
一直在寻找“保持简单”的建议,我的情况是数组始终为20x20=400个元素。现在我正在研究如何编写一个宏,可以扩展为400个“Nones”。 - Lukas

2
在 Rust 的夜间版本中,您可以使用 inline const。这是 @user4815162342 的答案的变体,但它不需要您声明一个单独的常量并重复类型:
#![feature(inline_const)]

let array: [Option<Box<Thing>>; SIZE] = [const { None }; SIZE];

直到这个问题稳定下来(希望很快),你也可以使用inline-const crate,但这需要你重复类型。

1
自 Rust 1.63 起,您可以使用 from_fn
let arr: [Option<Box<i64>>; 1000] = std::array::from_fn(|_| None);
assert!(arr.iter().all(|item| item.is_none()));

playground


请不要发布仅包含代码的答案。未来的读者会感激您解释为什么这个答案回答了问题,而不是从代码中推断出来。此外,由于这是一个旧的、回答得很好的问题,请解释它如何补充所有其他答案。 - Gert Arnold

1

使用arrayvec crate的另一种方法,可以轻松地推广到除了用固定值初始化所有内容之外的其他情况:

use arrayvec::ArrayVec;

let array = std::iter::repeat(None)
    .take(SIZE)
    .collect::<ArrayVec<Option<Box<Thing>>, SIZE>>()
    .into_inner()
    .unwrap();

(playground)

这是一个有关编程的网站链接。


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