如何 const 求值?
解读
在国内 Rust 岗位面试中,“const 求值”通常指编译期常量求值(const evaluation),即让编译器在编译阶段就把某些计算做完,而不是留到运行时。面试官想确认你能否:
- 区分
const、static、let的求值时机; - 写出能在编译期完成、且符合 const 上下文(const context) 约束的代码;
- 处理 const fn 稳定性、浮点运算、trait bound 等常见“踩坑点”。
知识点
- const 上下文:
const项、static项、enum判别值、const fn调用、#![feature(const_mut_refs)]等场景。 - const fn:函数体必须只包含编译期可确定的操作,不能调用非
const fn、不能堆分配、不能使用unsafe块(稳定版)。 - 常量求值限制:稳定版下浮点运算、比较、类型转换(
as)已稳定,但Vec、HashMap、Mutex等仍不可。 - 编译期递归:
const fn支持递归,但深度受编译器实现限制,溢出会报 “evaluation limit exceeded”。 - 内建宏:
concat!、env!、include_bytes!等本身就是编译期展开,可出现在const项中。 - nightly 特性:
#![feature(const_mut_refs)]、const_vec_string等,可在面试中提及,但需强调“线上代码必须用稳定版”。
答案
下面给出一段稳定版即可通过的示例,演示如何把一个编译期已知的数组在 const fn 里求和,并作为数组长度使用——这是国内面试官最爱追问的“const 求值落地场景”。
// 稳定 Rust 1.79+ 可直接编译
const BASE: &[u8] = b"rust";
// 1. const fn 内部只能使用已稳定的运算
const fn sum_len(a: &[u8], b: &[u8]) -> usize {
let mut total = 0;
let mut i = 0;
// 显式循环,稳定版已支持
while i < a.len() {
total += a[i] as usize;
i += 1;
}
let mut j = 0;
while j < b.len() {
total += b[j] as usize;
j += 1;
}
total
}
// 2. 编译期完成求值,结果作为数组长度
const LEN: usize = sum_len(BASE, b"2025");
// 3. 用在类型系统里,证明是真正的编译期常量
type Buffer = [u8; LEN];
fn main() {
let buf: Buffer = [0; LEN];
println!("{}", buf.len()); // 运行期无计算,直接输出 14
}
要点回顾:
sum_len必须是const fn,否则不能出现在LEN的初始化表达式中。- 所有输入(
BASE、b"2025")都是编译期可见的字节串,满足 const 上下文要求。 - 最终
LEN被用作数组长度,证明求值发生在编译期;若写错一步,编译器会立刻报错,符合 Rust “编译通过即正确” 的文化。
拓展思考
- 泛型常量求值:在稳定版使用
const N: usize做数组长度时,若还想在const fn里做复杂运算,可借助 关联常量 与 泛型特化 技巧,但需避免触发 “const 泛型参数未确定” 错误。 - const 与 static 区别:
static拥有固定内存地址,可带可变内部状态(static mut);而const只是内联字面量,每次使用都可能复制一份,面试中常被追问“为何不用static”。 - 未来演进:Rust 2024 版本计划进一步稳定
const async fn与const trait,届时可在编译期做更复杂的零成本抽象;作为候选人,可主动提及 “关注 RFC 3220、3495” 体现技术敏感度。 - 实际落地:在嵌入式
no_std场景,用const计算 CRC 表、比特反转表,可把原本运行时几十毫秒的计算直接归零,显著降低 MCU 唤醒时长;面试时给出这类量化收益,能让答案更具说服力。