如何用 #[repr(u8)] 控制枚举大小?

解读

国内面试官问“如何用 #[repr(u8)] 控制枚举大小”时,真正想考察的是你对 Rust 内存布局、ABI 可控性以及 FFI/嵌入式场景下“字节级对齐”的理解深度
他们并不满足于“加属性就能变小”这种表面答案,而是希望听到:

  1. 默认 discriminant 类型如何决定枚举大小;
  2. 为什么 u8 能把 64 位机器上的 tag 从 8 字节压到 1 字节;
  3. 什么时候会触发编译器拒绝;
  4. 与 C 交互时如何防止“布局踩踏”;
  5. 在高性能场景里如何利用 repr(u8) 做“零成本状态机”。
    一句话:面试官要的是“能把内存省到字节、还能讲出风险”的候选人。

知识点

  1. 默认布局:Rust 枚举默认使用 isize 作为 discriminant,对齐到机器字长,64 位下 tag 占 8 字节,导致空枚举也占 8 字节。
  2. #[repr(Int)] 族:显式指定 discriminant 的整型,Int ∈ {u8, u16, u32, u64, i8…},同时保持 Rust 的“tagged union”语义不变。
  3. 大小计算规则:
    size = max(variant_payload) 的对齐后大小 + discriminant 大小,再整体对齐到 max(align(max_payload), align(discriminant))。
    因此把 discriminant 从 8 字节压到 1 字节,可直接省 7 字节,且整体对齐可能从 8 降到 1
  4. 安全性边界:
    • 当变体带数据时,repr(u8) 不会做任何位级压缩,不会引入未定义行为
    • 若手动给 discriminant 赋值超出 u8 范围(>255),编译器 直接报错,而非静默截断;
    • 与 C 交互时,必须保证 C 端结构体也用 uint8_t 作为 tag,否则 ABI 不一致。
  5. 零成本状态机:在嵌入式或网络协议解析中,用 repr(u8) 枚举做阶段标记,既保证单字节存储,又能用 match 获得分支预测友好的跳转表
  6. 与 repr(C) 区别:repr(C) 只保证 C 兼容的 布局顺序,不压缩 tag;repr(u8) 既压缩 tag 又保留 Rust 语义,两者可叠加:#[repr(C, u8)],此时布局与 C 的 tagged union 完全一致,方便裸指针转换。

答案

#[repr(u8)]          // 强制 discriminant 为 u8
enum State {
    Init    = 0,
    Running = 1,
    Done    = 2,
}

fn main() {
    use std::mem;
    println!("size = {}", mem::size_of::<State>()); // 输出 1
}

步骤拆解

  1. 在 enum 前加 #[repr(u8)],编译器把 tag 类型从默认 isize 改为 u8;
  2. 由于三个变体都不带数据,payload 大小为 0,最终大小等于 1 字节;
  3. 若变体带数据,例如 Running(u32),则大小为 4(payload)+ 1(tag)再对齐到 4,共 5 字节,但对齐到 4 后实际占 8 字节
  4. 如果尝试 #[repr(u8)] enum Foo { X = 256 },编译器 立刻报错literal out of range for u8
  5. 与 C 交互时,C 端对应定义
    typedef struct { uint8_t tag; union { ... } data; } State;
    
    即可安全 transmute。

拓展思考

  1. 单字节枚举的位级技巧:在嵌入式寄存器映射中,可把 repr(u8) 枚举再包一层 #[repr(transparent)] struct Reg(u8)实现“强类型 + 单字节 MMIO”,既防止误写非法状态,又保证零开销。
  2. 与 const-generic 结合:做协议解析时,用 #[repr(u8)] enum Phase<const N: u8>编译期即可算出最大合法 discriminant,避免手工维护魔数。
  3. 风险案例:某国产数据库曾把 #[repr(u8)] 枚举通过 FFI 传给 C++,但 C++ 端 tag 声明为 enum class : uint32_t,导致高位 3 字节未初始化,在 ARM 大端机器上触发 UB。修复方案是 Rust 端改用 #[repr(C, u32)],保持两边宽度一致。
  4. 面试加分话术
    “除了省内存,repr(u8) 还能让状态机数组直接映射到 flash 段,做到常量表与代码段同址,减少一次 load 指令;在 48 MHz 的 Cortex-M0 上,我实测把协议解析延迟从 2.1 µs 降到 1.3 µs。”
    这类量化数据是国内面试官最愿意听到的“工程落地感”。