如何编写 const 泛型?
解读
在国内 Rust 岗位面试中,const 泛型(稳定版 1.51 起)是区分“写过业务”与“写过库”的试金石。面试官通常不会只问“语法怎么写”,而是围绕“为什么需要、边界在哪、如何降级兼容”层层追问,以此判断候选人对编译期计算、ABI、向后兼容、标准库源码的熟悉度。回答时必须给出可编译、可跑、可单元测试的代码片段,并主动说出“稳定版本要求”“报错信息长什么样”“如何与 trait 搭配”这三点,才能拿到高分。
知识点
- 语法骨架:
struct Foo<const N: usize>;impl<const N: usize> Foo<N>;fn bar<const M: i32>() {} - 允许的类型:整型、布尔、字符(
char)与&'static str(1.77 起);不允许f32/f64或自定义结构体 - 默认值写法:
struct Buffer<T, const N: usize = 1024>([T; N]); - where 子句 过滤:
impl<const N: usize> Buffer<u8, N> where [u8; N]: Default {} - 与 trait 搭配:
- 无法把
const N直接写进 trait 定义,但可用关联常量绕开 - 使用
typenum做非稳定版前的兼容方案
- 无法把
- 数组
[T; N]的里程碑:有了 const 泛型后,标准库才真正实现“数组 impl Default/Iterator/FromIterator”,不再需要array_impl!宏 - 常见编译错误:
error: unconstrained generic constant→ 缺少where [T; N]:约束error: generic parameters must not be used inside of non trivial constant values→ 表达式里用了泛型参数,需改用#
- 版本要求:1.51 稳定可用;1.79 开始支持
~const Destruct等更复杂的 const trait bound - 性能视角:const 泛型零运行时开销,编译期单态化后等同于手写
struct Foo_16 { buf: [u8; 16] } - 工程落地:
- 嵌入式
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.45,如何用宏 +
typenum模拟 const 泛型?
答:用typenum::U128等标记类型,再写macro_rules! impl_for_each,缺点是每个长度都要显式列出来,编译时间爆炸。 - 与
#[no_std]结合:在裸机环境,const 泛型数组彻底取代heapless::Vec的宏生成,减少 30% 编译时间;但需注意.bss段大小随N线性增长。 - 泛型表达式(nightly
generic_const_exprs)风险:fn foo<const N: usize>() -> [u8; N + 1]目前仍可能 ICE(内部编译器错误),生产环境务必关闭该 feature
- 与 FFI 打交道:
#[repr(C)] struct Foo<const N: usize> { buf: [u8; N] }的 ABI 与 C 端uint8_t buf[N]完全对等,可直接&foo.buf as *const _传指针,无需Box
- 面试反问环节:
- “贵司的加密模块是否用 const 泛型做 key 长度校验?”
- “在 1.51 之前,你们怎么解决
ArrayVec的 trait 统一问题?”
通过反问体现你对版本演进、历史债务、跨平台部署的深度关注,可加分。