# 有限状态建模:带数据的代数状态
学习目标:理解“使无效状态不可表示(Make Invalid States Unrepresentable)”的设计原则,掌握带状态关联数据的代数数据类型(ADT)与密封层级建模。
前置知识与范围:结构体与枚举。本篇聚焦后台异步任务的三种互斥状态及其专属上下文数据的严格建模。
# 问题与边界
为一个异步任务建模三种互斥状态,每种状态仅持有其合法的上下文数据:
Pending(等待排队中):持有重试次数retries: u32。Success(执行成功):持有最终输出结果字符串result: String。Failed(执行失败):持有错误状态码code: i32与错误信息message: String。
核心设计边界:
- 状态互斥:一个任务在任一瞬间必须且仅能处于上述三种状态之一。
- 杜绝脏状态:在
Pending状态下,数据结构在类型系统层面绝不能容纳result或code字段;禁止使用所有字段全可选的大平铺对象。
# 关键差异
| 语言 | 建模机制 | 状态数据存储 | 保证非法状态不可表达 |
|---|---|---|---|
| Rust | 枚举变体(带数据的代数类型 ADT) | 变体直接内联结构体/元组 | 编译期强类型保证,无空指针漏洞 |
| Go | 接口包裹具体私有结构体或带 Tag 结构体 | 各结构体独立拥有字段 | 通过接口实现封闭分发 |
| Java | 密封接口(Sealed Interface,Java 17+) | record 作为具体许可实现 |
permits 限制派生,配合模式匹配 |
| JavaScript | 对象字面量带判别标签(tag / status) |
扁平对象但根据标签持有特定字段 | 依靠代码契约与运行时分派 |
| TypeScript | 判别联合类型(Discriminated Union) | 带有公用标签字面量的对象联合 | 严格类型收窄,访问未定义分支字段直接报编译错 |
| Kotlin | 密封类(sealed class / sealed interface) |
继承密封基类的数据类 | 编译器静态掌握全部子类闭集 |
| Python | dataclass 配合 Union 类型提示 |
独立 dataclass 实例 | 依靠静态检查工具(mypy)推导 |
| C++ | std::variant 标签联合体(C++17) |
变体内部联合类型存储 | 类型安全联合体,避免裸 union 内存危险 |
# 各语言实现
# Rust
Rust#[derive(Debug, PartialEq)]
enum TaskState {
Pending { retries: u32 },
Success(String),
Failed { code: i32, message: String },
}
fn describe_state(state: &TaskState) -> String {
match state {
TaskState::Pending { retries } => format!("pending, retries={retries}"),
TaskState::Success(res) => format!("success: {res}"),
TaskState::Failed { code, message } => format!("failed({code}): {message}"),
}
}
fn main() {
let s1 = TaskState::Pending { retries: 2 };
let s2 = TaskState::Success("data_v1".to_string());
let s3 = TaskState::Failed { code: 500, message: "timeout".to_string() };
assert_eq!(describe_state(&s1), "pending, retries=2");
assert_eq!(describe_state(&s2), "success: data_v1");
assert_eq!(describe_state(&s3), "failed(500): timeout");
println!("{}", describe_state(&s2));
}
Rust 的 enum 是标准的一等公民代数类型,内存布局自动优化为紧凑的标签联合体(Tagged Union)。
# Go
Gopackage main
import "fmt"
type TaskState interface {
isTaskState()
Describe() string
}
type Pending struct {
Retries uint32
}
func (Pending) isTaskState() {}
func (p Pending) Describe() string {
return fmt.Sprintf("pending, retries=%d", p.Retries)
}
type Success struct {
Result string
}
func (Success) isTaskState() {}
func (s Success) Describe() string {
return fmt.Sprintf("success: %s", s.Result)
}
type Failed struct {
Code int
Message string
}
func (Failed) isTaskState() {}
func (f Failed) Describe() string {
return fmt.Sprintf("failed(%d): %s", f.Code, f.Message)
}
func main() {
var s TaskState = Success{Result: "data_v1"}
if s.Describe() != "success: data_v1" {
panic("mismatch")
}
fmt.Println(s.Describe())
}
Go 缺乏原生联合类型,惯用包内私有未导出方法标记接口(如 isTaskState())以防止外部非法实现该状态。
# Java
Javapublic class Main {
public sealed interface TaskState permits Pending, Success, Failed {}
public record Pending(int retries) implements TaskState {}
public record Success(String result) implements TaskState {}
public record Failed(int code, String message) implements TaskState {}
public static String describeState(TaskState state) {
if (state instanceof Pending p) {
return "pending, retries=" + p.retries();
} else if (state instanceof Success s) {
return "success: " + s.result();
} else if (state instanceof Failed f) {
return "failed(" + f.code() + "): " + f.message();
}
throw new IllegalStateException();
}
public static void main(String[] args) {
TaskState s1 = new Pending(2);
TaskState s2 = new Success("data_v1");
TaskState s3 = new Failed(500, "timeout");
assert "pending, retries=2".equals(describeState(s1));
assert "success: data_v1".equals(describeState(s2));
assert "failed(500): timeout".equals(describeState(s3));
System.out.println(describeState(s2));
}
}
Java 17 引入的 sealed interface 限制了状态子类型的数量,为模式匹配穷尽检查奠定了基础。
# JavaScript
JavaScriptconst assert = {
equal: (a, b) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a, b) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn, errType) => {
try { fn(); throw new Error("expected throw"); } catch (e) {
if (e.message === "expected throw") throw e;
if (errType && !(e instanceof errType)) throw e;
}
}
};
function describeState(state) {
switch (state.status) {
case "pending":
return `pending, retries=${state.retries}`;
case "success":
return `success: ${state.result}`;
case "failed":
return `failed(${state.code}): ${state.message}`;
default:
throw new Error("unknown state");
}
}
const s1 = { status: "pending", retries: 2 };
const s2 = { status: "success", result: "data_v1" };
const s3 = { status: "failed", code: 500, message: "timeout" };
assert.equal(describeState(s1), "pending, retries=2");
assert.equal(describeState(s2), "success: data_v1");
assert.equal(describeState(s3), "failed(500): timeout");
console.log(describeState(s2));
JavaScript 约定公用判别属性 status 来区分不同状态分支。
# TypeScript
TypeScriptconst assert = {
equal: (a: unknown, b: unknown) => { if (a !== b) throw new Error(`${a} !== ${b}`); },
deepEqual: (a: unknown, b: unknown) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${JSON.stringify(a)} !== ${JSON.stringify(b)}`); },
throws: (fn: () => void, errType?: unknown) => {
try { fn(); throw new Error("expected throw"); } catch (e: unknown) {
if (e instanceof Error && e.message === "expected throw") throw e;
if (typeof errType === "function" && !(e instanceof errType)) throw e;
}
}
};
interface Pending {
readonly status: "pending";
readonly retries: number;
}
interface Success {
readonly status: "success";
readonly result: string;
}
interface Failed {
readonly status: "failed";
readonly code: number;
readonly message: string;
}
type TaskState = Pending | Success | Failed;
function describeState(state: TaskState): string {
switch (state.status) {
case "pending":
return `pending, retries=${state.retries}`;
case "success":
return `success: ${state.result}`;
case "failed":
return `failed(${state.code}): ${state.message}`;
}
}
const s1: TaskState = { status: "pending", retries: 2 };
const s2: TaskState = { status: "success", result: "data_v1" };
const s3: TaskState = { status: "failed", code: 500, message: "timeout" };
assert.equal(describeState(s1), "pending, retries=2");
assert.equal(describeState(s2), "success: data_v1");
assert.equal(describeState(s3), "failed(500): timeout");
console.log(describeState(s2));
TypeScript 的判别联合(Discriminated Union)在分支中根据 status 自动缩小类型作用域;如果在 case "pending" 下试图访问 state.result,会触发编译错误。
# Kotlin
Kotlinsealed interface TaskState {
data class Pending(val retries: Int) : TaskState
data class Success(val result: String) : TaskState
data class Failed(val code: Int, val message: String) : TaskState
}
fun describeState(state: TaskState): String {
return when (state) {
is TaskState.Pending -> "pending, retries=${state.retries}"
is TaskState.Success -> "success: ${state.result}"
is TaskState.Failed -> "failed(${state.code}): ${state.message}"
}
}
fun main() {
val s1: TaskState = TaskState.Pending(2)
val s2: TaskState = TaskState.Success("data_v1")
val s3: TaskState = TaskState.Failed(500, "timeout")
check(describeState(s1) == "pending, retries=2")
check(describeState(s2) == "success: data_v1")
check(describeState(s3) == "failed(500): timeout")
println(describeState(s2))
}
Kotlin 的 sealed interface 与 when 强绑定,省略 else 分支即可得到编译期穷尽检查。
# Python
Pythonfrom dataclasses import dataclass
from typing import Union
@dataclass(frozen=True)
class Pending:
retries: int
@dataclass(frozen=True)
class Success:
result: str
@dataclass(frozen=True)
class Failed:
code: int
message: str
TaskState = Union[Pending, Success, Failed]
def describe_state(state: TaskState) -> str:
match state:
case Pending(retries=r):
return f"pending, retries={r}"
case Success(result=res):
return f"success: {res}"
case Failed(code=c, message=m):
return f"failed({c}): {m}"
if __name__ == "__main__":
s1 = Pending(2)
s2 = Success("data_v1")
s3 = Failed(500, "timeout")
assert describe_state(s1) == "pending, retries=2"
assert describe_state(s2) == "success: data_v1"
assert describe_state(s3) == "failed(500): timeout"
print(describe_state(s2))
Python 3.10 的结构化模式匹配(Structural Pattern Matching)配合 dataclass 可以直接解构类模式。
# C++
C++#include <iostream>
#include <variant>
#include <string>
#include <cassert>
struct Pending { int retries; };
struct Success { std::string result; };
struct Failed { int code; std::string message; };
using TaskState = std::variant<Pending, Success, Failed>;
// 辅助重载仿函数模板
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::string describe_state(const TaskState& state) {
return std::visit(overloaded {
[](const Pending& p) { return "pending, retries=" + std::to_string(p.retries); },
[](const Success& s) { return "success: " + s.result; },
[](const Failed& f) { return "failed(" + std::to_string(f.code) + "): " + f.message; }
}, state);
}
int main() {
TaskState s1 = Pending{2};
TaskState s2 = Success{"data_v1"};
TaskState s3 = Failed{500, "timeout"};
assert(describe_state(s1) == "pending, retries=2");
assert(describe_state(s2) == "success: data_v1");
assert(describe_state(s3) == "failed(500): timeout");
std::cout << describe_state(s2) << "\n";
}
C++17 的 std::variant 提供了类型安全联合体,配合 std::visit 避免了 C 风格裸 union 类型转换的未定义风险。
# 迁移误区与适用限制
- 滥用“大平铺结构体”(God State Struct):定义一个包含
status, retries, result, code, message全部字段的对象,并将所有字段设为可空。这种做法极易产生“status 为 Pending 却不小心读取了上次残留的 result”等脏状态 Bug。 - 动态类型语言标签拼写错误:在没有 TypeScript 或编译期密封约束时,
status === "pendng"这样的拼写笔误会直接静默滑入default异常处理。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本success: data_v1
各状态专属字段解构与字符串转换断言全部通过。