如何 const 求值?

解读

在国内 Rust 岗位面试中,“const 求值”通常指编译期常量求值(const evaluation),即让编译器在编译阶段就把某些计算做完,而不是留到运行时。面试官想确认你能否:

  1. 区分 conststaticlet 的求值时机;
  2. 写出能在编译期完成、且符合 const 上下文(const context) 约束的代码;
  3. 处理 const fn 稳定性浮点运算trait bound 等常见“踩坑点”。

知识点

  1. const 上下文const 项、static 项、enum 判别值、const fn 调用、#![feature(const_mut_refs)] 等场景。
  2. const fn:函数体必须只包含编译期可确定的操作,不能调用非 const fn不能堆分配、不能使用 unsafe 块(稳定版)。
  3. 常量求值限制:稳定版下浮点运算、比较、类型转换(as)已稳定,但 VecHashMapMutex 等仍不可。
  4. 编译期递归const fn 支持递归,但深度受编译器实现限制,溢出会报 “evaluation limit exceeded”
  5. 内建宏concat!env!include_bytes! 等本身就是编译期展开,可出现在 const 项中。
  6. 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 的初始化表达式中。
  • 所有输入(BASEb"2025")都是编译期可见的字节串,满足 const 上下文要求
  • 最终 LEN 被用作数组长度,证明求值发生在编译期;若写错一步,编译器会立刻报错,符合 Rust “编译通过即正确” 的文化。

拓展思考

  1. 泛型常量求值:在稳定版使用 const N: usize 做数组长度时,若还想在 const fn 里做复杂运算,可借助 关联常量泛型特化 技巧,但需避免触发 “const 泛型参数未确定” 错误。
  2. const 与 static 区别static 拥有固定内存地址,可带可变内部状态(static mut;而 const 只是内联字面量,每次使用都可能复制一份,面试中常被追问“为何不用 static”。
  3. 未来演进:Rust 2024 版本计划进一步稳定 const async fnconst trait,届时可在编译期做更复杂的零成本抽象;作为候选人,可主动提及 “关注 RFC 3220、3495” 体现技术敏感度。
  4. 实际落地:在嵌入式 no_std 场景,用 const 计算 CRC 表、比特反转表,可把原本运行时几十毫秒的计算直接归零,显著降低 MCU 唤醒时长;面试时给出这类量化收益,能让答案更具说服力。