如何用零成本状态机?

解读

“零成本状态机”在国内 Rust 面试里通常有两层含义:

  1. 运行时零开销:状态转移不依赖堆分配、虚函数、动态分发或额外线程,编译后等价于手写 C 状态机。
  2. 内存安全零成本:借助 Rust 所有权与类型系统,在编译期把“非法状态转移”变成编译错误,无需运行时检查。

面试官想听你如何把“状态”编码进类型系统,用泛型状态参数 + 结构体零大小字段实现“状态即类型、转移即构造”,最终生成与 switch-case 一样快的机器码,同时杜绝“未初始化就调用”“重复关闭”等 bug。

知识点

  1. 类型级状态机(Type-State Pattern)
    把状态作为结构体的泛型参数 struct S<State>,利用 PhantomData<State> 零大小字段携带状态信息,编译期即可拒绝非法调用。

  2. 零大小类型(ZST)与单态化
    PhantomData<State> 不占空间,S<StateA>S<StateB> 单态化后仍是同一个机器字,无运行时膨胀

  3. 转移即构造
    消费 self 返回新类型 S<NextState>,旧状态值被 move 后无法再使用,天然防止“二次转移”。

  4. const 泛型与枚举判别式优化
    若状态数量固定,可用 const STATE: u8 做判别,编译器会优化成跳转表,与手写 C 状态机指令数相同

  5. async/await 状态机
    Rust 编译器把 async fn 编译成匿名状态机,每个 .await 点对应一个状态,零运行时调度器开销,也是“零成本”体现。

答案

下面给出国内面试官最认可的“类型级状态机”模板,编译后只占 1 字节,却能在编译期禁止“未连接就发送”这类错误。

use std::marker::PhantomData;

// 1. 状态标签都是 ZST
struct Disconnected;
struct Connected;
struct Closed;

// 2. 连接层状态机,State 参数决定当前合法操作
struct Connection<State> {
    _pd: PhantomData<State>,
    // 真实资源句柄,大小与状态无关
    fd: i32,
}

impl Connection<Disconnected> {
    pub fn new(fd: i32) -> Self {
        Connection { _pd: PhantomData, fd }
    }
    // 唯一转移入口:消耗 self,返回 Connected
    pub fn connect(self, addr: &str) -> Connection<Connected> {
        real_connect(self.fd, addr);
        Connection { _pd: PhantomData, fd: self.fd }
    }
}

impl Connection<Connected> {
    pub fn send(&self, data: &[u8]) -> usize {
        real_send(self.fd, data)
    }
    pub fn close(self) -> Connection<Closed> {
        real_close(self.fd);
        Connection { _pd: PhantomData, fd: self.fd }
    }
}

impl Connection<Closed> {
    pub fn release(self) {
        real_release(self.fd);
    }
}

// 3. 使用示例:编译期即可拦截错误顺序
fn demo() {
    let conn = Connection::<Disconnected>::new(3);
    let conn = conn.connect("127.0.0.1:80");   // 必须 connect 后才能 send
    conn.send(b"hello");                       // OK
    let conn = conn.close();                   // 转移成 Closed
    // conn.send(b"world");                    // 编译错误:Closed 没有 send 方法
    conn.release();
}

关键点

  • PhantomData<State> 零大小,生成的结构体与 C 里的 int fd 开销一致
  • 状态转移通过“消费 self”完成,旧状态无法再使用,杜绝悬垂状态。
  • 不同状态的方法在编译期单态化,无虚表、无分支,指令数等价于手写 switch(state)

拓展思考

  1. 可恢复状态机
    若需要“断开后可重新连接”,可引入 impl From<Connection<Closed>> for Connection<Disconnected>,但务必显式转换,防止隐式回退。

  2. const 状态压缩
    当状态超过 3 种且转移矩阵稀疏,可用 const STATE: u8 代替泛型,配合 #[repr(u8)] enummatch 让编译器生成跳转表,同样零成本。

  3. 异步场景
    在 tokio 中把上述状态机改成 async fn,每个 .await 点由编译器自动生成状态编号,无需手写 Poll 循环,性能对标 epoll 手写回调,却保持内存安全。

  4. 嵌入式中断
    #[no_std] 环境下,把状态机做成 static mut STATE: u8 配合 cortex-matomic 操作,中断级零拷贝,Rust 也能通过 unsafe 边界封装出 safe API,通过编译即无数据竞争

掌握以上套路,面试时可直接反问:“贵司业务里哪个模块最担心非法状态转移?我可以现场用类型级状态机把 bug 变成编译错误。” 这种编译期担保、运行时零开销的表述,是国内 Rust 面试官最愿意给高分的亮点。