如何 const 泛型数组?

解读

在国内 Rust 面试中,“const 泛型数组” 通常指“如何在泛型参数里把数组长度作为编译期常量传进去”,从而让函数、结构体或 trait 在不同长度数组上复用同一份泛型代码,同时保持零成本抽象编译期检查
面试官想确认三点:

  1. 你是否真的理解 const 泛型(const N: usize)的语法与稳定状态;
  2. 能否写出无需 trait 包装、无需宏、无需 Vec 分配的纯栈上实现;
  3. 是否知道当前 stable 通道的长度上限 32 的“老限制”已被 const 泛型打破,以及由此带来的数组 Copy/Clone/Debug 等自动实现的边界条件。

知识点

  1. const 泛型参数struct Foo<const N: usize>fn bar<const N: usize>(a: [T; N]),稳定自 1.51。
  2. 数组类型 [T; N] 的“长度”必须是编译期常量,const 泛型正好填补这一空缺。
  3. 泛型常量表达式(generic_const_exprs)仍属 nightly,面试时只需回答 stable 能力即可,不要提 #![feature(generic_const_exprs)],否则会被追问 unstable 风险。
  4. trait 边界中若出现 N + 12 * N 等表达式,stable 会报错,需用类型级技巧绕过;面试现场只需指出“目前 stable 不支持任意表达式”即可。
  5. 数组作为函数参数时,默认按值传递会移动整个栈拷贝,大数组需用 &[T; N]&[T] 避免复制;但题目问“泛型数组”而非“切片”,优先展示按值版本以体现对 const 泛型的信心。

答案

// 1. 结构体持有任意长度数组
#[derive(Debug, Clone, Copy)]
struct Vector<T, const N: usize> {
    data: [T; N],
}

impl<T: Default + Copy, const N: usize> Vector<T, N> {
    // 2. 关联函数:生成零向量
    fn zero() -> Self {
        Self { data: [T::default(); N] }
    }

    // 3. 方法:逐元素相加
    fn add(&self, other: &Self) -> Self {
        let mut result = self.data;
        for i in 0..N {
            result[i] = self.data[i] + other.data[i];
        }
        Self { data: result }
    }
}

// 4. 独立函数:接收任意长度数组并返回其长度
fn array_len<T, const N: usize>(_arr: &[T; N]) -> usize {
    N
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_const_generic_array() {
        let a: Vector<i32, 5> = Vector::zero();
        let b = Vector { data: [1, 2, 3, 4, 5] };
        let c = a.add(&b);
        assert_eq!(c.data, [1, 2, 3, 4, 5]);
        assert_eq!(array_len(&c.data), 5);
    }
}

要点强调

  • <const N: usize> 写在泛型列表里,与类型参数位置无关,但必须显式声明;
  • 数组长度 N 直接出现在类型 [T; N],编译器会为每个用到的 N 单态化一份代码,无运行时开销
  • stable Rust 已支持任意大小的 const 泛型数组,不再受 32 限制,可直接 let x: [u8; 1024] = [0; 1024];

拓展思考

  1. 如果面试官追问“如何返回不同长度的数组?”
    答:函数签名必须在编译期确定长度,因此无法根据运行时值返回不同长度;可用 Vec<T>Box<[T]> 做堆分配,也可用 const 泛型工厂模式让调用者指定长度。

  2. “数组长度做 trait 边界”
    例如只想实现 Vector<f32, 3> 的叉乘,可用零成本特化

    impl Vector<f32, 3> {
        fn cross(&self, other: &Self) -> Self { ... }
    }
    

    无需宏,编译期只实例化一份,体现 Rust 的“静态分派 + 精确单态化”。

  3. “const 泛型与 trait 对象不能共存”
    dyn Vector<T, N> 是非法的,因为 N 是编译期常量,而 trait 对象要求类型擦除;面试时可补充:“如需动态分发,可擦除长度到切片 &[T] 或自定义胖指针”

  4. “未来 nightly 的 generic_const_exprs 会带来什么”
    可简单提及:“允许 fn foo<const N: usize>() -> [u8; N + 1],但当前 stable 仍需宏或 GAT 曲线救国”,表现出对语言演进的持续跟踪意识,国内大厂看重这一点。