Comprehensive Rust 精讲:Typestate 模式——用类型系统在编译期强制合法状态流转
Comprehensive Rust 精讲Typestate 模式——用类型系统在编译期强制合法状态流转【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust导读本篇文章基于 Google 的 Rust 课程 Comprehensive Rust 中 Idiomatic Rust 部分关于Typestate类型状态模式的完整章节围绕一个真实场景——序列化器——展开先用朴素实现暴露忘记收尾、状态混用的运行时隐患再用 Typestate 模式把状态编码进类型最后结合泛型解决嵌套结构带来的类型爆炸问题。读完本文你将掌握 Typestate 的核心思想通过消费值产生新值来实现状态转移、如何在 Rust 中用它构造仅允许合法操作的 API以及它与泛型结合后如何表达递归的嵌套状态机。一、问题背景如何保证值只能做当前状态下合法的事1.1 需求场景在 typestate-pattern.md 中课程提出了一个核心问题如何根据一个值的当前状态确保只有合法的操作能被允许执行答案是让 Rust 的类型系统而不是运行时检查来保证这一点。课程用一个Serializer序列化器作为贯穿全章的案例。它的工作方式是流式的先开始一个 struct然后写入若干字段最后结束 struct 并得到最终的字符串输出。1.2 朴素实现及其缺陷课程给出的第一版实现问题篇把一切方法都挂在同一个Serializer结构体上# // Copyright 2025 Google LLC # // SPDX-License-Identifier: Apache-2.0 # use std::fmt::Write as _; #[derive(Default)] struct Serializer { output: String, } impl Serializer { fn serialize_struct_start(mut self, name: str) { let _ writeln!(mut self.output, {name} {{); } fn serialize_struct_field(mut self, key: str, value: str) { let _ writeln!(mut self.output, {key}{value};); } fn serialize_struct_end(mut self) { self.output.push_str(}\n); } fn finish(self) - String { self.output } } fn main() { let mut serializer Serializer::default(); serializer.serialize_struct_start(User); serializer.serialize_struct_field(id, 42); serializer.serialize_struct_field(name, Alice); // serializer.serialize_struct_end(); // ← Oops! Forgotten println!({}, serializer.finish()); }这段代码可以正常编译运行但它的输出是不完整、语法错误的User { id42; nameAlice;问题在于我们忘记调用serialize_struct_end()编译器却毫无怨言。这正是课程想要指出的痛点——忘记做某件必须做的事在运行时才暴露而不是在编译期。1.3 两种补救思路及其代价课程进一步分析了两种修复方向思路一手动维护内部状态 返回Result让serialize_struct_field()或finish()在状态非法时返回Result。课程明确指出这种做法的两个缺点对实现者来说很容易出错Rust 的类型系统无法帮你校验状态转移的正确性一切靠人肉维护对使用者来说徒增负担明明是在源码层面用错了 API属于编译期就应发现的问题却被迫在运行时处理Result错误。思路二推荐把合法的状态转移直接建模到类型系统里也就是本篇文章的主角——Typestate 模式。课程预告在下一张幻灯片中我们将应用 Typestate 模式让错误用法在编译期就无法通过并且不可能调用不兼容的方法也不可能忘记做必须的动作。二、Typestate 模式入门以序列化器为例2.1 核心思想消费值、产出新值在 typestate-example.md 中课程给出了 Typestate 的定义Typestate 模式把值的运行时状态的一部分编码进类型中从而在编译期阻止非法或不恰当的操作。它的关键机制是状态转移通过消费一个值并产生一个新的值来完成。每一步只有对当前状态合法的方法才是可用的——因为其他方法根本不在这个类型的impl块里编译器自然找不到它们。2.2 引入SerializeStruct状态类型课程用两个类型来表达序列化 struct 之前和正在序列化 struct 中两种状态# // Copyright 2025 Google LLC # // SPDX-License-Identifier: Apache-2.0 # use std::fmt::Write as _; #[derive(Default)] struct Serializer { output: String, } struct SerializeStruct { serializer: Serializer, } impl Serializer { fn serialize_struct(mut self, name: str) - SerializeStruct { writeln!(mut self.output, {name} {{).unwrap(); SerializeStruct { serializer: self } } fn finish(self) - String { self.output } } impl SerializeStruct { fn serialize_field(mut self, key: str, value: str) - Self { writeln!(mut self.serializer.output, {key}{value};).unwrap(); self } fn finish_struct(mut self) - Serializer { self.serializer.output.push_str(}\n); self.serializer } } fn main() { let serializer Serializer::default() .serialize_struct(User) .serialize_field(id, 42) .serialize_field(name, Alice) .finish_struct(); println!({}, serializer.finish()); }注意调用方式的显著变化从可变借用 分步调用变成了链式调用builder 风格。方法签名中mut self与- 新类型的组合正是消费—产出模式的体现。2.3 状态流转图课程用bob图清晰地画出了Serializer的使用流程------------ serialize struct ----------------- | Serializer | ------------------ | SerializeStruct | ------ ------------ ----------------- | | | ^ | | | | | finish struct | | serialize field | | ----------------------------- ------------------ | --- finish从图中可以读出Serializer上只有两个出口serialize_struct进入 struct 状态finish直接产出StringSerializeStruct上只有两个出口serialize_field留在原地- Selffinish_struct回到Serializer。2.4 为什么这解决了问题课程逐条说明了 Typestate 带来的保证一开始我们只有Serializer它只允许开始序列化一个 struct一旦调用.serialize_struct(...)所有权就移入SerializeStruct值此后只能调用与 struct 字段序列化相关的方法原来的Serializer不再可访问——这就防止了模式混用例如在一个 struct 序列化到一半时又开一个新的 struct也防止了过早调用finish()只有调用.finish_struct()之后才拿回Serializer此时输出才能被终结或复用如果忘记调用finish_struct()就提前 drop 掉SerializeStruct那么内部的Serializer也会被一起 drop——不完整的输出不可能泄漏进系统对照之下如果像上一节那样把所有方法都放在Serializer上任何人都可以跳过关键步骤或混用序列化流程编译器毫无察觉。课程还透露这个例子受到了 Serde 的Serializertrait 的启发——Serde 内部就用 Typestate 来保证序列化遵循合法的结构。对于更深入的 Serde 自定义序列化器实现可以参考其官方文档《Implementing a Serializer》的相关章节。三、更复杂的场景嵌套结构与类型爆炸3.1 新需求支持嵌套 struct 与 list在 typestate-advanced.md 中课程把需求升级了在上一个序列化器的基础上支持嵌套结构和列表。也就是说一个属性property的值可以是字符串、一个嵌套 struct、或者一个 listlist 的元素又可以是字符串或嵌套 struct……如此递归。课程给出了理想状态流转图----------- -------------------------- | | | | | | V | V | V | | serializer -- structure -- property -- list - | | ^ | ^ V | | | | | ----------- | String | | --------------------------观察这张图能看出三个关键特征转移是递归的list 里可以有 structstruct 里可以有 propertyproperty 又可以产生 list返回类型取决于出现在哪里一个 struct 结束后如果它位于根层应返回Serializer如果它是另一个 struct 的属性应返回SerializeStruct如果它是 list 中的元素应返回SerializeList每个上下文都需要一条回到父级的路径。3.2 纯具体类型方案的困境课程在进阶篇中给出了一个只定义类型骨架、方法留作 TODO的示例来说明问题的严重性# // Copyright 2025 Google LLC # // SPDX-License-Identifier: Apache-2.0 # struct Serializer {/* [...] */} struct SerializeStruct {/* [...] */} struct SerializeStructProperty {/* [...] */} struct SerializeList {/* [...] */} impl Serializer { // TODO, implement: // // fn serialize_struct(self, name: str) - SerializeStruct // fn finish(self) - String } impl SerializeStruct { // TODO, implement: // // fn serialize_property(mut self, name: str) - SerializeStructProperty // TODO, // How should we finish this struct? This depends on where it appears: // - At the root level: return Serializer // - As a property inside another struct: return SerializeStruct // - As a value inside a list: return SerializeList // // fn finish(self) - ??? } impl SerializeStructProperty { // TODO, implement: // // fn serialize_string(self, value: str) - SerializeStruct // fn serialize_struct(self, name: str) - SerializeStruct // fn serialize_list(self) - SerializeList // fn finish(self) - SerializeStruct } impl SerializeList { // TODO, implement: // // fn serialize_string(mut self, value: str) - Self // fn serialize_struct(mut self, value: str) - SerializeStruct // fn serialize_list(mut self) - SerializeList // TODO: // Like SerializeStruct::finish, the return type depends on nesting. // // fn finish(mut self) - ??? }课程明确指出这个方向会同时引入重复与结构复杂性两个问题而且更致命的是撞上了一个类型系统限制如果不为每一种嵌套上下文根层、struct 内、list 内复制一份变体就无法干净地表达finish()到底应该返回什么。仅靠具体类型这种方案会走向类型爆炸 手工接线的失控局面。课程的结论是下一章将看到泛型如何用更少的样板代码建模递归流程同时仍然在编译期强制合法操作。四、Typestate × Generics用泛型追踪父级上下文4.1 把状态参数化在 typestate-generics.md 及其子页面中课程给出了完整解法。核心思路用一个泛型参数S表示当前状态 父级上下文状态类型本身做成薄薄的包装# // Copyright 2025 Google LLC # // SPDX-License-Identifier: Apache-2.0 # use std::fmt::Write as _; struct SerializerS { // [...] indent: usize, buffer: String, state: S, } struct Root; struct StructS(S); struct ListS(S); struct PropertyS(S);以上定义摘自仓库中的 typestate-generics.rs 源码文件。这里SerializerS本身携带了数据indent缩进级别、buffer输出缓冲区以及当前状态state: S。而Root、StructS、ListS、PropertyS这些类型只是状态的标签Root是零大小类型ZST代表根层StructS(S)、ListS(S)、PropertyS(S)是单字段的元组结构体字段类型S就是父级上下文——正是这个嵌套的S让状态机拥有了记忆能力。课程特别强调这些标记类型不引入任何内存或运行时开销它们至多含有一个零大小类型唯一的职责就是通过类型系统强制正确的 API 用法。4.2 各状态的方法实现源码级完整实现可以在 typestate-generics.rs 中看到仓库将其按 ANCHOR 分块。以下是各状态的核心方法。Root 状态只能开 struct 或结束对应子页面 root.mdimpl SerializerRoot { fn new() - Self { // [...] Self { indent: 0, buffer: String::new(), state: Root } } fn serialize_struct(mut self, name: str) - SerializerStructRoot { // [...] writeln!(self.buffer, {name} {{).unwrap(); Serializer { indent: self.indent 1, buffer: self.buffer, state: Struct(self.state), } } fn finish(self) - String { // [...] self.buffer } }课程在此处的注释强调在根这一层唯一允许构造的是Struct且只有从根层才能把Serializer终结为String。Struct 状态只允许加 property或结束回到父级对应子页面 struct.mdimplS SerializerStructS { fn serialize_property(mut self, name: str) - SerializerPropertyStructS { // [...] write!(self.buffer, {}{name}: , .repeat(self.indent * 2)).unwrap(); Serializer { indent: self.indent, buffer: self.buffer, state: Property(self.state), } } fn finish_struct(mut self) - SerializerS { // [...] self.indent - 1; writeln!(self.buffer, {}}}, .repeat(self.indent * 2)).unwrap(); Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 } } }注意finish_struct(mut self) - SerializerSS就是这个 struct 出现的上下文父级泛型让同一个方法在根层、嵌套 struct 内、list 内都能正确回到各自的父级——这正是上一节类型爆炸问题的解药。Property 状态属性值可以是字符串、嵌套 struct 或 list对应子页面 property.mdimplS SerializerPropertyStructS { fn serialize_struct(mut self, name: str) - SerializerStructStructS { // [...] writeln!(self.buffer, {name} {{).unwrap(); Serializer { indent: self.indent 1, buffer: self.buffer, state: Struct(self.state.0), } } fn serialize_list(mut self) - SerializerListStructS { // [...] writeln!(self.buffer, [).unwrap(); Serializer { indent: self.indent 1, buffer: self.buffer, state: List(self.state.0), } } fn serialize_string(mut self, value: str) - SerializerStructS { // [...] writeln!(self.buffer, {value},).unwrap(); Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 } } }可以看到从PropertyStructS状态出发serialize_string直接产出SerializerStructS属性写完后回到外层 structserialize_struct产出SerializerStructStructS进入嵌套 struct其父级是原来的 structserialize_list产出SerializerListStructS进入 list其父级是原来的 struct。List 状态元素可以是字符串或嵌套 struct也可以结束 list完整实现见 typestate-generics.rsimplS SerializerListS { fn serialize_struct(mut self, name: str) - SerializerStructListS { // [...] writeln!(self.buffer, {}{name} {{, .repeat(self.indent * 2)).unwrap(); Serializer { indent: self.indent 1, buffer: self.buffer, state: Struct(self.state), } } fn serialize_string(mut self, value: str) - Self { // [...] writeln!(self.buffer, {}{value},, .repeat(self.indent * 2)).unwrap(); self } fn finish_list(mut self) - SerializerS { // [...] self.indent - 1; writeln!(self.buffer, {}], .repeat(self.indent * 2)).unwrap(); Serializer { indent: self.indent, buffer: self.buffer, state: self.state.0 } } }一个值得注意的细节SerializerListS的impl块是针对任意S泛型定义的而SerializerPropertyStructS的impl块则把S绑定在了特定的嵌套形状PropertyStructS上。课程指出对所有状态通用的方法可以定义在任意SerializerS上——这正是泛型带来的代码复用收益。4.3 完整状态图全部状态把上面所有impl拼接起来课程在 complete.md 中给出了覆盖全部状态的最终流转图-------------------- -------------- ------------------------- --------------- | SerializerRoot | | SerializerStructS | | -------------------- -------------- ------------------------- ----------- | finish struct serialize | | | | serialize | string or | | | ---------- property V struct | | finish | | --------------------------- | | V | | SerializerPropertyS | -------- | -------- finish | --------------------------- | | String | struct | serialize | | -------- | list V | | ----------------------- finish | ----- | SerializerListS | -- list | ----------------------- | serialize | | list or string ^ | | or finish list | | ----------------- |这张图直观地展示了任意层级的嵌套都通过StructS/PropertyS/ListS中不断累积的泛型参数S串成一条回到父级的链而finish_struct/finish_list每执行一次就把链缩短一层。4.4 一段可以真正跑起来的完整用法仓库源码 typestate-generics.rs 的main函数演示了完整用法——一个三层嵌套 列表的序列化请求fn main() { #[rustfmt::skip] let serializer Serializer::new() .serialize_struct(Foo) .serialize_property(bar) .serialize_struct(Bar) .serialize_property(baz) .serialize_list() .serialize_string(abc) .serialize_struct(Baz) .serialize_property(partial) .serialize_string(def) .serialize_property(empty) .serialize_struct(Empty) .finish_struct() .finish_struct() .finish_list() .finish_struct() .finish_struct(); let output serializer.finish(); println!({output}); }这段链式调用虽然写起来深但每一层的缩进层次都受到类型系统的检查少写一个.finish_struct()或.finish_list()或者在不该出现的地方调用某个方法编译器都会立刻报错。源码中甚至列出了几条注定编译失败的调用用于演示类型系统的拦截能力// These will all fail at compile time: // Serializer::new().serialize_list(); // Serializer::new().serialize_string(foo); // Serializer::new().serialize_struct(Foo).serialize_string(bar); // Serializer::new().serialize_struct(Foo).serialize_list(); // Serializer::new().serialize_property(foo);Serializer::new().serialize_list()——根层不允许直接开列表Serializer::new().serialize_string(foo)——根层不允许直接写字符串Serializer::new().serialize_struct(Foo).serialize_string(bar)——在Struct状态下不允许直接写字符串必须先serialize_propertySerializer::new().serialize_struct(Foo).serialize_list()——Struct状态不允许直接开列表Serializer::new().serialize_property(foo)——Root状态下没有serialize_property方法。这五条注释正是 Typestate 模式价值的最直接证据所有非法操作都在编译期被拒绝。五、模式边界Typestate 不是什么银弹5.1 仍然存在的能力缺口课程在 complete.md 中坦诚地列出了 Typestate 依然无法处理的问题空属性名或非法属性名类型系统无法表达name 非空这类语义约束。课程建议用本仓库其他章节讲过的 newtype 模式 来修复用包装类型在构造时就保证非空重复的属性名这属于运行时才能发现的语义错误可以在StructS里维护已用属性名集合并通过Result返回值来处理。5.2 与Result的结合可恢复的校验失败如果某些校验确实需要在运行时做课程给出的做法是把方法签名改成返回Result从而在失败时保留恢复的机会。例如struct PropertySerializeErrorS { kind: PropertyError, serializer: SerializerStructS, } implS SerializerStructS { fn serialize_property( self, name: str, ) - ResultSerializerPropertyStructS, PropertySerializeErrorS { /* ... */ } }注意错误类型里带着serializer: SerializerStructS——失败时把尚未消耗掉的序列化器原样返回给调用方使其可以修正后重试或优雅地终止。这是 Typestate编译期结构约束与Result运行时语义校验两种机制互补协作的典范。5.3 实用主义视角与真实世界案例课程最后给出了务实的评价这个 API 虽然强大但并不总是符合人体工程学。生产环境的序列化器通常倾向于更简单的 API把 Typestate 模式留给那些必须强制的不变量。换言之Typestate 是一种有代价的武器——类型签名会变得复杂、链式调用要求严格按序、写错就编译不过。它适合用来守卫关键不变量而不是给所有方法无差别地套上。课程提到的一个真实案例是rustls的ClientConfig其 builder 方法使用泛型 Typestate 引导用户安全、正确地完成 TLS 客户端配置的每个步骤。此外如本文开头所述Serde 内部也利用 Typestate 保证序列化结构的合法性——这些都说明该模式在工业级 Rust 库中是真实落地、行之有效的。六、小结与扩展阅读回顾整个章节Typestate 模式的完整学习路径是问题篇朴素Serializer暴露忘记收尾、状态混用的运行时风险引出把合法状态转移建模到类型系统的思路示例篇用Serializer→SerializeStruct两个类型实现最简 Typestate展示消费—产出的状态转移机制进阶篇嵌套 struct / list 需求让纯具体类型方案陷入类型爆炸暴露返回类型取决于上下文的难题泛型篇用SerializerS 标记类型StructS/PropertyS/ListS建模递归状态机finish通过S回到任意父级分步实现Root → Struct → Property → 完整实现完整源码在 typestate-generics.rs。如果你想继续深挖相关的类型系统技巧本仓库的 Leveraging the Type System 部分还提供了高度互补的章节newtype 模式把语义约束收进构造器、借出检查器不变量用生命周期与借用关系表达不变量、token 类型用零大小标记类型做编译期开关以及 RAII把资源生命周期绑定到析构。它们与 Typestate 一起构成了把更多错误从运行时挪到编译期的完整工具箱。一句话总结Typestate 模式通过消费值、产出新值把状态转移刻进类型签名配合泛型即可表达任意嵌套的合法流程——让非法操作在编译期无处遁形让忘记收尾这类错误根本没有编译通过的机会。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →