如何编写 const 泛型?

解读

在国内 Rust 岗位面试中,const 泛型(稳定版 1.51 起)是区分“写过业务”与“写过库”的试金石。面试官通常不会只问“语法怎么写”,而是围绕“为什么需要、边界在哪、如何降级兼容”层层追问,以此判断候选人对编译期计算、ABI、向后兼容、标准库源码的熟悉度。回答时必须给出可编译、可跑、可单元测试的代码片段,并主动说出“稳定版本要求”“报错信息长什么样”“如何与 trait 搭配”这三点,才能拿到高分。

知识点

  1. 语法骨架:struct Foo<const N: usize>impl<const N: usize> Foo<N>fn bar<const M: i32>() {}
  2. 允许的类型:整型、布尔、字符char)与 &'static str(1.77 起);不允许 f32/f64 或自定义结构体
  3. 默认值写法:struct Buffer<T, const N: usize = 1024>([T; N]);
  4. where 子句 过滤:impl<const N: usize> Buffer<u8, N> where [u8; N]: Default {}
  5. 与 trait 搭配:
    • 无法把 const N 直接写进 trait 定义,但可用关联常量绕开
    • 使用 typenum 做非稳定版前的兼容方案
  6. 数组 [T; N] 的里程碑:有了 const 泛型后,标准库才真正实现“数组 impl Default/Iterator/FromIterator”,不再需要 array_impl!
  7. 常见编译错误:
    • error: unconstrained generic constant → 缺少 where [T; N]: 约束
    • error: generic parameters must not be used inside of non trivial constant values → 表达式里用了泛型参数,需改用 #![feature(generic_const_exprs)](nightly)
  8. 版本要求:1.51 稳定可用;1.79 开始支持 ~const Destruct 等更复杂的 const trait bound
  9. 性能视角:const 泛型零运行时开销,编译期单态化后等同于手写 struct Foo_16 { buf: [u8; 16] }
  10. 工程落地:
    • 嵌入式 Buffer<u8, 128> 做 DMA
    • 网络 StaticVec<T, 1518> 替代 Vec 避免堆分配
    • 加密算法固定长度 key,如 AesBlock<const ROUNDS: usize>

答案

// 1. 最小可编译示例:固定容量栈向量
#![allow(dead_code)]
use std::ops::{Index, IndexMut};

#[derive(Debug)]
pub struct StaticVec<T, const N: usize> {
    data: [T; N],
    len: usize,
}

impl<T: Default + Copy, const N: usize> StaticVec<T, N> {
    pub fn new() -> Self {
        Self {
            data: [T::default(); N],
            len: 0,
        }
    }

    pub fn push(&mut self, item: T) -> Result<(), &'static str> {
        if self.len >= N {
            return Err("overflow");
        }
        self.data[self.len] = item;
        self.len += 1;
        Ok(())
    }

    pub fn as_slice(&self) -> &[T] {
        &self.data[..self.len]
    }
}

// 2. 为所有长度实现统一 trait
impl<T, const N: usize> Index<usize> for StaticVec<T, N> {
    type Output = T;
    fn index(&self, i: usize) -> &T {
        &self.data[i]
    }
}

// 3. 非泛型长度特化:只为 16 字节提供 hex 打印
impl<T: std::fmt::LowerHex, const N: usize> StaticVec<T, N> {
    pub fn hex_print(&self) -> String
    where
        [T; N]: ,          // 约束长度合法
    {
        self.as_slice()
            .iter()
            .map(|x| format!("{:02x}", x))
            .collect::<Vec<_>>()
            .join(" ")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() {
        let mut v: StaticVec<u8, 4> = StaticVec::new();
        v.push(0xde).unwrap();
        v.push(0xad).unwrap();
        assert_eq!(v.hex_print(), "de ad 00 00");
    }
}

关键点讲解

  • struct 后面紧跟 <const N: usize>const 关键字不可省
  • 数组初始化 [T::default(); N] 依赖 T: Copy,否则需用 std::array::from_fn(1.82+)
  • 测试用例里 StaticVec<u8, 4> 会在编译期单态化出独立二进制,无运行时长度检查开销
  • 如果面试官追问“如何支持动态长度拼接”,可答:再包一层 enum { Inline(StaticVec<T, N>), Heap(Vec<T>) } 做 small-vector 优化

拓展思考

  1. 向下兼容:如果公司代码库仍要支持 1.45,如何用宏 + typenum 模拟 const 泛型
    答:用 typenum::U128 等标记类型,再写 macro_rules! impl_for_each,缺点是每个长度都要显式列出来,编译时间爆炸。
  2. #[no_std] 结合:在裸机环境,const 泛型数组彻底取代 heapless::Vec 的宏生成,减少 30% 编译时间;但需注意 .bss 段大小随 N 线性增长。
  3. 泛型表达式(nightly generic_const_exprs)风险:
    • fn foo<const N: usize>() -> [u8; N + 1] 目前仍可能 ICE(内部编译器错误),生产环境务必关闭该 feature
  4. 与 FFI 打交道:
    • #[repr(C)] struct Foo<const N: usize> { buf: [u8; N] } 的 ABI 与 C 端 uint8_t buf[N] 完全对等,可直接 &foo.buf as *const _ 传指针,无需 Box
  5. 面试反问环节:
    • “贵司的加密模块是否用 const 泛型做 key 长度校验?”
    • “在 1.51 之前,你们怎么解决 ArrayVec 的 trait 统一问题?”
      通过反问体现你对版本演进、历史债务、跨平台部署的深度关注,可加分。