一、什么是类型状态模式

类型状态模式(Type-State Pattern)是 Rust 中的一种编译期状态机实现技巧,核心思想是:在类型层面约束对象的行为,使错误的状态转换在编译时即被拒绝,而非运行时检查。

传统的状态模式依赖运行时枚举或 trait object,每次状态转换都需要 match 或 dynamic dispatch。而类型状态模式利用泛型类型参数和所有权系统,让 Rust 编译器在编译期静态验证状态转换的合法性。

二、基础原理:用类型参数编码状态

核心技巧是使用零大小类型(ZST)作为状态标记:

struct Uninitialized;
struct Ready;
struct Completed;

struct Task<S> {
    name: String,
    state: std::marker::PhantomData<S>,
}

impl Task<Uninitialized> {
    fn new(name: String) -> Self {
        Task { name, state: PhantomData }
    }
    fn prepare(self) -> Task<Ready> {
        println!("{} 准备完成", self.name);
        Task { name: self.name, state: PhantomData }
    }
}

impl Task<Ready> {
    fn execute(self) -> Task<Completed> {
        println!("{} 执行完成", self.name);
        Task { name: self.name, state: PhantomData }
    }
}

impl Task<Completed> {
    fn result(&self) -> &str {
        &self.name
    }
}

PhantomData<S> 是零大小类型,不占用运行时开销。但编译器知道 Task<Uninitialized>、Task<Ready>、Task<Completed> 是不同的类型。

三、消费型 Builder 实战

类型状态模式最经典的用途是强制 Builder 顺序调用:

use std::marker::PhantomData;

struct NoHost;
struct NoPort;
struct WithHost;
struct WithPort;

struct HttpRequestBuilder<H, P> {
    host: Option<String>,
    port: Option<u16>,
    path: String,
    _h: PhantomData<H>,
    _p: PhantomData<P>,
}

impl HttpRequestBuilder<NoHost, NoPort> {
    fn new(path: &str) -> Self {
        HttpRequestBuilder {
            host: None, port: None, path: path.to_string(),
            _h: PhantomData, _p: PhantomData,
        }
    }
}

impl<P> HttpRequestBuilder<NoHost, P> {
    fn host(self, h: &str) -> HttpRequestBuilder<WithHost, P> {
        HttpRequestBuilder {
            host: Some(h.to_string()),
            port: self.port, path: self.path,
            _h: PhantomData, _p: PhantomData,
        }
    }
}

impl<H> HttpRequestBuilder<H, NoPort> {
    fn port(self, p: u16) -> HttpRequestBuilder<H, WithPort> {
        HttpRequestBuilder {
            host: self.host, port: Some(p), path: self.path,
            _h: PhantomData, _p: PhantomData,
        }
    }
}

impl HttpRequestBuilder<WithHost, WithPort> {
    fn build(self) -> String {
        format!("http://{}:{}{}", self.host.unwrap(), self.port.unwrap(), self.path)
    }
}

这样 HttpRequestBuilder::new("/api").host("example.com").port(8080).build() 能编译通过,但如果遗漏 .host() 或 .port(),编译器会报错——因为只有 WithHost + WithPort 类型才有 build() 方法。

四、状态转换约束与子状态机

更复杂的场景可以约束哪些状态可以互相转换:

mod sealed {
    pub trait Sealed {}
    impl Sealed for super::Idle {}
    impl Sealed for super::Running {}
    impl Sealed for super::Error {}
}

pub trait State: sealed::Sealed {}

struct Idle;
struct Running { handle: std::thread::Thread }
struct Error { code: i32 }

impl State for Idle {}
impl State for Running {}
impl State for Error {}

struct Service<S: State> {
    inner: ServiceInner,
    _marker: PhantomData<S>,
}

impl Service<Idle> {
    fn start(self) -> Result<Service<Running>, Service<Error>> {
        match do_start(&self.inner) {
            Ok(handle) => Ok(Service { inner: self.inner, _marker: PhantomData }),
            Err(code) => Err(Service { inner: self.inner, _marker: PhantomData }),
        }
    }
}

impl Service<Running> {
    fn stop(self) -> Service<Idle> {
        // 只有 Running 状态可以停止
        Service { inner: self.inner, _marker: PhantomData }
    }
    fn restart(self) -> Result<Service<Running>, Service<Error>> {
        // Running -> Idle -> Running 的合法路径
        self.stop().start()
    }
}

// Idle 状态没有 stop 方法!编译器会阻止 service_idle.stop() 的调用

五、与 async 结合的类型状态

Rust async 生态中类型状态尤其有用,因为 .poll() 方法不能保证被正确调用:

use std::pin::Pin;
use std::task::{Context, Poll};

struct NotPolled;
struct Polled;

struct LazyFuture<F, S> {
    factory: F,
    state: S,
}

impl<T, F: FnOnce() -> T> LazyFuture<F, NotPolled> {
    fn new(factory: F) -> Self {
        LazyFuture { factory, state: NotPolled }
    }
}

<T, F: FnOnce() -> T> std::future::Future for LazyFuture<F, NotPolled> {
    type Output = T;

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<T> {
        let this = unsafe { self.get_unchecked_mut() };
        let result = (this.factory)();
        this.state = Polled;
        Poll::Ready(result)
    }
}

六、性能分析

方式运行时开销二进制大小状态错误检测
运行时枚举match 分支正常运行时 panic
trait object虚函数调用正常运行时 None
类型状态模式零开销(ZST优化)单态化膨胀编译期拒绝

单态化的代价是二进制体积膨胀,但每个具体类型的代码都是零开销抽象,编译器可以完全内联优化。

七、总结

类型状态模式是 Rust 独有的强大技巧,它利用零大小类型参数和编译期单态化,将运行时状态机约束上移到类型系统层面。适用场景包括:Builder 模式强制调用顺序、状态机转换合法性保证、编译期防止非法协议消息等。代价是编译时间增加和二进制膨胀,因此在状态数较多时需要权衡。

类型状态模式与 Rust 所有权系统的结合,使得“让不可能的状态成为不可能”这句话从口号变成了编译器级别的可执行保证。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部