# 处理不同状态:穷尽检查与模式匹配
学习目标:掌握代数状态消费中的模式解构(Pattern Matching)、类型收窄(Narrowing)与编译期穷尽检查(Exhaustiveness Checking)。
前置知识与范围:有限状态建模。聚焦如何安全消费互斥状态,并保证新增状态时编译器主动拦截漏写分支。
# 问题与边界
针对任务状态执行调度动作(返回下一步动作指令字符串):
- 若为
Pending(retries):- 带有守卫条件:若
retries < 3返回"retry";若retries >= 3返回"abandon"。
- 带有守卫条件:若
- 若为
Success(result):返回"save: " + result。 - 若为
Failed(code, message):返回"notify: " + message。
核心检验标准:
- 分支安全:在分支内部必须能安全访问对应状态携带的字段(如
result),绝不能发生未定义属性访问。 - 穷尽性保障:若未来在状态定义中增加一个
Cancelled状态,语言工具链必须在编译期报错(而非等到生产环境运行时跑进未知分支)。
# 关键差异
| 语言 | 模式匹配语法 | 守卫条件(Guard) | 穷尽检查(Exhaustiveness)保障方式 |
|---|---|---|---|
| Rust | match state { ... } |
if retries < 3 => ... |
编译器原生强制,漏写枚举变体直接编译失败 |
| Go | switch s := state.(type) |
分支内手写 if |
编译器不检查穷尽,漏写分支默认静默跳过 |
| Java | Java 21 switch (state) 模式匹配 |
when r < 3 |
对 sealed 体系编译器原生强制穷尽检查 |
| JavaScript | switch (state.status) |
分支内常规 if |
仅能在 default 中抛出运行时异常 |
| TypeScript | switch (state.status) |
分支内 if |
在 default 中使用 never 类型守卫,编译期拦截 |
| Kotlin | when (state) |
分支内手写 if |
对 sealed 体系省略 else 触发强制穷尽检查 |
| Python | Python 3.10 match state: |
case Pending(r) if r < 3: |
使用 typing.assert_never 配合 mypy 静态检查 |
| C++ | std::visit(overloaded{...}, var) |
lambda 内部条件 | std::visit 要求重载集合必须穷尽所有 variant 类型 |
# 各语言实现
# Rust
Rustenum TaskState {
Pending { retries: u32 },
Success(String),
Failed { code: i32, message: String },
}
fn next_action(state: &TaskState) -> String {
match state {
// 模式守卫(Match Guard)
TaskState::Pending { retries } if *retries < 3 => "retry".to_string(),
TaskState::Pending { .. } => "abandon".to_string(),
TaskState::Success(res) => format!("save: {res}"),
TaskState::Failed { message, .. } => format!("notify: {message}"),
}
}
fn main() {
assert_eq!(next_action(&TaskState::Pending { retries: 1 }), "retry");
assert_eq!(next_action(&TaskState::Pending { retries: 3 }), "abandon");
assert_eq!(next_action(&TaskState::Success("data_v1".to_string())), "save: data_v1");
assert_eq!(next_action(&TaskState::Failed { code: 500, message: "disk full".to_string() }), "notify: disk full");
println!("action: {}", next_action(&TaskState::Pending { retries: 1 }));
}
Rust 编译器具备严格的代数模式分析能力;删除任何一个非守卫分支,编译器均会立即报错拒绝生成二进制。
# Go
Gopackage main
import "fmt"
type TaskState interface {
isTaskState()
}
type Pending struct{ Retries int }
func (Pending) isTaskState() {}
type Success struct{ Result string }
func (Success) isTaskState() {}
type Failed struct {
Code int
Message string
}
func (Failed) isTaskState() {}
func NextAction(state TaskState) string {
switch s := state.(type) {
case Pending:
if s.Retries < 3 {
return "retry"
}
return "abandon"
case Success:
return "save: " + s.Result
case Failed:
return "notify: " + s.Message
default:
panic("unhandled state") // Go 必须依靠运行时防御
}
}
func main() {
if NextAction(Pending{Retries: 1}) != "retry" {
panic("mismatch")
}
if NextAction(Success{Result: "data_v1"}) != "save: data_v1" {
panic("mismatch")
}
fmt.Printf("action: %s\n", NextAction(Pending{Retries: 1}))
}
Go 的 switch .(type) 无法在编译期强制要求覆盖所有接口实现,生产实践中建议在 default 显式 panic 防御。
# 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 nextAction(TaskState state) {
// Java 21 switch 模式匹配 + when 守卫
return switch (state) {
case Pending p when p.retries() < 3 -> "retry";
case Pending p -> "abandon";
case Success s -> "save: " + s.result();
case Failed f -> "notify: " + f.message();
}; // 密封体系无需 default,编译器强制穷尽检查!
}
public static void main(String[] args) {
assert "retry".equals(nextAction(new Pending(1)));
assert "abandon".equals(nextAction(new Pending(3)));
assert "save: data_v1".equals(nextAction(new Success("data_v1")));
System.out.println("action: " + nextAction(new Pending(1)));
}
}
Java 21 的模式匹配表达式对于 sealed 类自动触发穷尽校验,若增加子类而未更新 switch,编译直接报错。
# 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 nextAction(state) {
switch (state.status) {
case "pending":
return state.retries < 3 ? "retry" : "abandon";
case "success":
return `save: ${state.result}`;
case "failed":
return `notify: ${state.message}`;
default:
throw new Error(`Unhandled state: ${JSON.stringify(state)}`);
}
}
assert.equal(nextAction({ status: "pending", retries: 1 }), "retry");
assert.equal(nextAction({ status: "pending", retries: 3 }), "abandon");
assert.equal(nextAction({ status: "success", result: "data_v1" }), "save: data_v1");
console.log(`action: ${nextAction({ status: "pending", retries: 1 })}`);
JavaScript 在 default 分支抛出带有上下文详细信息的异常是防御漏判的唯一途径。
# 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 assertNever(x: never): never {
throw new Error(`Unexpected object: ${x}`);
}
function nextAction(state: TaskState): string {
switch (state.status) {
case "pending":
return state.retries < 3 ? "retry" : "abandon";
case "success":
return `save: ${state.result}`;
case "failed":
return `notify: ${state.message}`;
default:
// 穷尽性检查:若上面漏写分支,state 在此处不是 never,编译直接报错!
return assertNever(state);
}
}
assert.equal(nextAction({ status: "pending", retries: 1 }), "retry");
assert.equal(nextAction({ status: "success", result: "data_v1" }), "save: data_v1");
console.log(`action: ${nextAction({ status: "pending", retries: 1 })}`);
TypeScript 的 never 类型守卫是工业级前端代码保证状态覆盖率的关键技巧。
# 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 nextAction(state: TaskState): String {
// 作为表达式的 when:省略 else 强制穷尽检查
return when (state) {
is TaskState.Pending -> if (state.retries < 3) "retry" else "abandon"
is TaskState.Success -> "save: ${state.result}"
is TaskState.Failed -> "notify: ${state.message}"
}
}
fun main() {
check(nextAction(TaskState.Pending(1)) == "retry")
check(nextAction(TaskState.Pending(3)) == "abandon")
check(nextAction(TaskState.Success("data_v1")) == "save: data_v1")
println("action: ${nextAction(TaskState.Pending(1))}")
}
Kotlin 的 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 next_action(state: TaskState) -> str:
match state:
case Pending(r) if r < 3:
return "retry"
case Pending():
return "abandon"
case Success(result=res):
return f"save: {res}"
case Failed(message=msg):
return f"notify: {msg}"
case _:
raise ValueError("Unhandled state")
if __name__ == "__main__":
assert next_action(Pending(1)) == "retry"
assert next_action(Pending(3)) == "abandon"
assert next_action(Success("data_v1")) == "save: data_v1"
print(f"action: {next_action(Pending(1))}")
Python 3.10 的模式匹配语法支持在模式后直接跟 if 守卫条件。
# 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 next_action(const TaskState& state) {
return std::visit(overloaded {
[](const Pending& p) { return p.retries < 3 ? "retry" : "abandon"; },
[](const Success& s) { return "save: " + s.result; },
[](const Failed& f) { return "notify: " + f.message; }
}, state);
}
int main() {
assert(next_action(Pending{1}) == "retry");
assert(next_action(Pending{3}) == "abandon");
assert(next_action(Success{"data_v1"}) == "save: data_v1");
std::cout << "action: " << next_action(Pending{1}) << "\n";
}
C++ 中若 overloaded 漏掉任何一个 variant 内部类型的重载,模板实例化在编译期立即崩溃,提供可靠的穷尽性。
# 迁移误区与适用限制
- 滥用默认分支(
default/_):在模式匹配中过早使用default会直接摧毁编译器的穷尽检查能力。未来一旦向状态机添加了关键新状态(如Cancelled),程序会静默滑入default处理逻辑,导致极难定位的业务死锁或数据丢失。 - 守卫条件与完全穷尽:包含
if守卫的分支(如case Pending(r) if r < 3)在类型系统看来是不完全的,必须有不带守卫的备选模式(如case Pending())或者全集范围匹配,否则即使所有逻辑数值覆盖了,编译器仍可能判定未穷尽。
# 运行与验证
语言版本基线见示例基线。 所有程序运行后控制台输出均为:
纯文本action: retry
守卫分支跳转(重试与放弃)、成功保存与失败提取测试全部通过。