第 15 章 · 枚举、联合类型与模式匹配
解决什么问题:一个值只能是有限几种情况之一、每种情况带的数据又不一样时,怎么把“只有这几种”写进类型,处理时又怎么保证每一种都照顾到。
四种语言的态度:Python 有
Enum和match,穷尽检查交给类型检查器;JS 什么都没有,靠约定;TS 用字面量联合类型和never在编译期查漏;C# 有整数枚举和switch表达式,可辨识联合 2026 年还在 C# 15 预览版里;Go 没有枚举也没有和类型,用iota和接口代替。
支付接口返回的结果有三种:成功,带收款金额;被拒,带拒绝原因;处理中,什么也不带。程序要按种类生成一行提示。我们还希望以后接口多出第四种结果(比如“已退款”)时,所有漏处理它的地方都能被工具指出来。另外,订单有“已下单、已发货、已送达”三种状态,程序要数出状态一共有几种。
最简单的“有限几种情况”是枚举(enumeration):一组有名字的常量,比如订单的三种状态。和直接写 0、1、2 相比,名字能说明含义,拼错了会报错。各语言对枚举的理解差别很大:有的把它当作整数的别名,任何整数都能冒充一个状态;有的把每个成员做成独一无二的对象,非法的值根本造不出来。
支付结果比枚举多了一层:每种情况带的数据不同。类型论里把“既有 A 又有 B”的类型叫积类型(product type),结构体、元组、普通的类都是;把“要么是 A、要么是 B、要么是 C”的类型叫和类型(sum type),也叫标签联合(tagged union)或可辨识联合(discriminated union)。两者合称代数数据类型(algebraic data type,ADT):“积”和“和”指的是可能取值的个数:两个字段各有 3 种和 4 种取值,组合起来是 3 × 4 种;三种情况各有若干取值,合起来是它们的和。枚举可以看成每种情况都不带数据的和类型。
看上下两半的差别:上面每个值只有一个标签和这一种自己的数据,处理中只有标签;下面每个值都带着全部三个字段,虚线框里的字段对这种结果毫无意义,却仍然存在。用可空字段的写法,“成功但金额为空”“被拒却带着金额”这类不该存在的组合在类型上都是合法的;和类型让这些组合根本写不出来。
处理和类型的手段是模式匹配(pattern matching):按值的形状逐条比对,比对成功时把里面的数据取出来,绑定到变量上。和一连串 if 判断相比,它多了一项能力:编译器知道这个类型一共只有哪几种情况,能检查每一种是否都有分支处理,这叫穷尽性检查(exhaustiveness checking)。本章的五种写法在这件事上差别最大,对比表前的图会把它们放在一起。
Python 怎么做
Section titled “Python 怎么做”Python 的标准库有 enum 模块(3.4 起);3.10 起有 match 语句做结构模式匹配(structural pattern matching);和类型没有专门语法,用几个 dataclass 加类型标注里的联合 | 表示。穷尽检查不在解释器里,只在 mypy、Pyright 这类类型检查器里。
from dataclasses import dataclassfrom enum import Enumfrom typing import assert_never
class OrderStatus(Enum): CREATED = "已下单" SHIPPED = "已发货" DELIVERED = "已送达"
@dataclass(frozen=True)class Success: amount: int
@dataclass(frozen=True)class Declined: reason: str
@dataclass(frozen=True)class Pending: pass
type PaymentResult = Success | Declined | Pending
def describe(result: PaymentResult) -> str: match result: case Success(amount=amount): return f"成功:收款 {amount} 元" case Declined(reason=reason): return f"被拒:{reason}" case Pending(): return "处理中" case _: assert_never(result) # 漏掉一种时,类型检查器在这里报错
results: list[PaymentResult] = [Success(120), Declined("余额不足"), Pending()]for r in results: print(describe(r))print(f"订单状态共有 {len(OrderStatus)} 种")
# 输出:# 成功:收款 120 元# 被拒:余额不足# 处理中# 订单状态共有 3 种运行:python payments.py。case Success(amount=amount) 是类模式(class pattern):先检查 result 是不是 Success 的实例,再取出 amount 属性绑定到同名变量。除了类模式,case "已下单": 这样的字面量模式比较值,case [first, *rest]: 匹配序列,case {"kind": "success"}: 匹配字典;模式后面还可以加守卫(guard),如 case Success(amount=a) if a >= 10000:,条件为假就继续往下比。type PaymentResult = ... 是 3.12 的类型别名语法,只给检查器看。
assert_never(3.11 起在 typing 里)的参数类型是 Never,即“不可能有值”。检查器沿着 match 逐条排除:过了三个 case 之后,result 剩下的类型是空的,传给 assert_never 正好合法。给 PaymentResult 加上第四种 Refunded 而不改 describe,mypy --strict 会报 Argument 1 to "assert_never" has incompatible type "Refunded"; expected "Never";不跑检查器直接运行,走到这一行时抛出 AssertionError: Expected code to be unreachable, but got: Refunded(amount=5)。若标签只是字符串,typing.Literal["success", "declined"] 也能写出字面量的联合,检查器同样按它收窄类型。
底层怎么实现。 Enum 靠元类(metaclass)EnumType 工作:类体执行完后,元类把每个赋值改造成该类的一个实例,整个程序里只有这一个 OrderStatus.SHIPPED;len(OrderStatus) 和 for s in OrderStatus 调用的是元类上的 __len__、__iter__;OrderStatus("已取消") 查的是元类建好的“值到成员”字典,查不到就抛 ValueError: '已取消' is not a valid OrderStatus,所以非法的状态造不出来。
match 编译成普通的字节码,自上而下一条条试,没有跳转表。类模式对应 MATCH_CLASS 指令:先做 isinstance,再按关键字取属性;位置写法 Success(120) 靠类上的 __match_args__ 把位置对应到属性名,dataclass 会自动生成它。
- 裸写变量名是捕获,不是比较:
case CREATED:不会和外面的常量CREATED比,而是把任何值都绑定给新变量CREATED,后面的分支全部作废。它不是最后一条时编译器报SyntaxError: name capture 'CREATED' makes remaining patterns unreachable;是最后一条时不报错,却什么都能匹配。要比较常量必须写带点的名字,如case OrderStatus.CREATED:。 - 运行时不检查穷尽:没有
case _时,所有分支都不匹配,match静静结束,函数返回None,调用方打印出一行None。assert_never只把“漏了”变成运行时异常,写代码时提醒你的只有检查器。
JS/TS 怎么做
Section titled “JS/TS 怎么做”JavaScript 没有枚举,也没有联合类型。惯用做法是用冻结的对象(Object.freeze)模拟枚举,用一个字符串字段 kind 当标签,再用 switch 分派,这就是可辨识联合的“手工版”。
const OrderStatus = Object.freeze({ Created: "已下单", Shipped: "已发货", Delivered: "已送达",});
function describe(result) { switch (result.kind) { case "success": return `成功:收款 ${result.amount} 元`; case "declined": return `被拒:${result.reason}`; case "pending": return "处理中"; default: throw new Error(`未知的支付结果:${result.kind}`); }}
const results = [ { kind: "success", amount: 120 }, { kind: "declined", reason: "余额不足" }, { kind: "pending" },];for (const r of results) console.log(describe(r));console.log(`订单状态共有 ${Object.keys(OrderStatus).length} 种`);
// 输出:// 成功:收款 120 元// 被拒:余额不足// 处理中// 订单状态共有 3 种运行:node payments.js。Object.freeze 让 OrderStatus.Created = "x" 在非严格模式下静默失效(ES 模块和严格模式下抛 TypeError),也不能再加新成员。成员值也可以用 Symbol("已下单"):每个 Symbol 独一无二,内容相同的字符串冒充不了它;代价是不能直接序列化成 JSON。switch 用 === 比较,default 里抛错是唯一的兜底。
底层怎么实现。 这里没有任何新机制:枚举是普通对象,联合是一组形状不同的普通对象,switch 是逐个比较字符串。三种结果在 V8 里是三种隐藏类(hidden class),读 result.kind 处的内联缓存从单态变成多态,只是稍慢一点。
- 拼错标签没人提醒:
case "sucess":和{ kind: "Success" }都是合法代码,结果是成功的支付一律走到default抛错,或者在没写default的switch里返回undefined。 - 加第四种结果时没有工具指出漏处理的地方,只能搜索字符串,再等运行时的
default。
TypeScript 有 enum 关键字,分数字枚举和字符串枚举,会生成运行时对象;更常用的是字面量联合类型和可辨识联合,它们只存在于编译期。穷尽检查靠 never 类型。
enum OrderStatus { Created = "CREATED", Shipped = "SHIPPED", Delivered = "DELIVERED",}
type PaymentResult = | { kind: "success"; amount: number } | { kind: "declined"; reason: string } | { kind: "pending" };
function assertNever(value: never): never { throw new Error(`未处理的支付结果:${JSON.stringify(value)}`);}
function describe(result: PaymentResult): string { switch (result.kind) { case "success": return `成功:收款 ${result.amount} 元`; // 这里 result 已收窄为成功那一种 case "declined": return `被拒:${result.reason}`; case "pending": return "处理中"; default: return assertNever(result); // 漏掉一种时,这一行编译不过 }}
const results: PaymentResult[] = [ { kind: "success", amount: 120 }, { kind: "declined", reason: "余额不足" }, { kind: "pending" },];for (const r of results) console.log(describe(r));console.log(`订单状态共有 ${Object.values(OrderStatus).length} 种`);
// 输出:// 成功:收款 120 元// 被拒:余额不足// 处理中// 订单状态共有 3 种运行:tsx payments.ts。kind 的类型不是 string,而是 "success" 这样的字面量类型(literal type),三种对象类型用 | 并起来就是可辨识联合,kind 是辨识字段(discriminant)。在 case "success": 分支里,编译器把 result 收窄(narrowing)成成功那一种。never 是没有任何值的类型:三个分支处理完,default 里的 result 只剩 never,传给 assertNever 合法。加上第四种 { kind: "refunded"; amount: number } 而不改 describe,tsc --strict 报 error TS2345: Argument of type '{ kind: "refunded"; amount: number; }' is not assignable to parameter of type 'never'.,程序编译不过。即使不写 default,strict 下返回 string 的函数漏了分支也会报 TS2366(缺少结尾的 return)。
底层怎么实现。 联合类型、字面量类型、never 和收窄都在类型检查时完成,生成的 JS 和上一节的手工版逐字相同。enum 则不同,它是 TS 少数会生成运行时代码的语法:数字枚举编译成一个立即执行函数,往对象上同时写入 Created → 0 和 0 → "Created" 两个方向的映射(反向映射,reverse mapping);字符串枚举只有正向映射。所以本例用 Object.values 数出 3;若改成数字枚举,Object.keys 会得到 0、1、2、Created、Shipped、Delivered 六个键。
正因为 enum 要生成代码,Node 的类型剥离(type stripping)不支持它:本机 Node 22 直接运行这个文件,报 SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode,要加 --experimental-transform-types 或改用 tsx。TS 5.8 为此加了 --erasableSyntaxOnly 选项,打开后写 enum 报 TS1294。很多项目因此改用 as const 对象加字面量联合代替 enum。
- 数字枚举接受任意数字:TS 5.0 起直接写
let s: Status = 42会报错,但const n: number = 42; let s: Status = n;照样通过,运行时s就是 42,不是任何一个状态。 const enum要求编译器在使用处把成员替换成字面量,这需要看到声明它的文件。tsx、esbuild 和 Node 的类型剥离一次只看一个文件,引用别的文件里声明的declare const enum时无法替换,打开isolatedModules后 tsc 报 TS2748。- 类型只在编译期:从网络收到的 JSON 声称是
PaymentResult,kind却是"refund",编译器管不到,运行时走到assertNever抛错;外部数据要先校验。
C# 怎么做
Section titled “C# 怎么做”C# 有原生的 enum,本质是整数;模式匹配从 C# 7 起逐版加入,switch 表达式在 C# 8,record 和关系模式在 C# 9。截至 2026 年 10 月,本书使用的 C# 14(.NET 10)没有可辨识联合:union 关键字和用来封闭继承层级的 closed 修饰符都属于 C# 15,只在 .NET 11 的预览版里,预计随 2026 年 11 月的 .NET 11 正式发布。C# 14 的惯用替代是一个抽象 record 加几个派生 record,再用 switch 表达式分派。
using System;using System.Diagnostics;
PaymentResult[] results = [new Success(120), new Declined("余额不足"), new Pending()];foreach (var r in results) Console.WriteLine(Describe(r));Console.WriteLine($"订单状态共有 {Enum.GetValues<OrderStatus>().Length} 种");
static string Describe(PaymentResult result) => result switch{ Success { Amount: var amount } => $"成功:收款 {amount} 元", // 属性模式 Declined(var reason) => $"被拒:{reason}", // 位置模式,靠 record 生成的 Deconstruct Pending => "处理中", // 类型模式 _ => throw new UnreachableException($"未处理的支付结果:{result}"),};
enum OrderStatus { Created, Shipped, Delivered }
abstract record PaymentResult;sealed record Success(int Amount) : PaymentResult;sealed record Declined(string Reason) : PaymentResult;sealed record Pending : PaymentResult;
// 输出:// 成功:收款 120 元// 被拒:余额不足// 处理中// 订单状态共有 3 种运行:dotnet run payments.cs。switch 表达式里每一条是“模式 => 结果”:Pending 是类型模式(type pattern),Success { Amount: var amount } 是属性模式(property pattern),Declined(var reason) 是位置模式(positional pattern),靠 record 自动生成的 Deconstruct 方法拆开。C# 9 的关系模式(relational pattern)可以直接比大小,如 Success { Amount: >= 10000 } => "大额收款",写在普通成功分支前面。
为什么要写最后那条 _?编译器检查穷尽性时,只知道 PaymentResult 是一个抽象类,任何程序集都可以再派生一个子类,所以三条都写全了它仍认为不穷尽,构建时报 warning CS8509,指出 _ 模式未包含在内。补上 _ 消除警告,代价是加第四种结果时什么提示都没有,要到运行时才抛 UnreachableException。枚举则相反:对 OrderStatus 的 switch 表达式漏了 Delivered,构建时报 CS8509,指出 OrderStatus.Delivered 模式未包含在内;三个都写全也还有一条 CS8524,提醒 (OrderStatus)3 这样的未命名值没处理。这些都是警告,程序照样能编译运行。C# 15 的 closed record 和 union 正是为了让编译器知道“只有这几种”,写全了就不再需要 _。
底层怎么实现。 enum 是派生自 System.Enum 的值类型,内存里就是一个 int(可改成 byte、long 等),成员只是编译期常量,OrderStatus.Shipped 编译后就是整数 1。Enum.GetValues 通过反射读取类型元数据里的常量字段,数出 3 个。record 是普通的类,编译器替它生成构造函数、按值比较的 Equals、ToString 和 Deconstruct。
switch 表达式由编译器构造成一张决策图(decision DAG):类型模式编译成 IL 的 isinst 类型测试,属性模式是读属性再比较。编译器认为不穷尽而又没有 _ 时,会在末尾自动插入抛出 SwitchExpressionException 的代码,所以漏掉的值在运行时不会静默通过。
- 枚举可以装任何整数:
OrderStatus s = (OrderStatus)42;编译、运行都没问题,打印出来是42。从数据库或 JSON 读进来的值要用Enum.IsDefined检查。 - 枚举上的
switch语句(不是表达式)漏了新成员不报警告:给OrderStatus加了Cancelled,旧的switch (s) { case ...: }静默跳过它。
Go 怎么做
Section titled “Go 怎么做”Go 没有枚举,也没有和类型。替代办法是 const 块加 iota 定义一组带类型的整数常量,用接口加类型断言或 type switch 表示“几种类型之一”,再用一个不导出的方法把接口“封闭”在本包里。
package main
import "fmt"
type OrderStatus int
const ( Created OrderStatus = iota // 0 Shipped // 1 Delivered // 2 orderStatusCount // 3:不是状态,只用来数个数)
// PaymentResult 只有本包能实现:方法名小写,别的包写不出这个方法。type PaymentResult interface{ isPaymentResult() }
type Success struct{ Amount int }type Declined struct{ Reason string }type Pending struct{}
func (Success) isPaymentResult() {}func (Declined) isPaymentResult() {}func (Pending) isPaymentResult() {}
func describe(result PaymentResult) string { switch r := result.(type) { case Success: return fmt.Sprintf("成功:收款 %d 元", r.Amount) case Declined: return "被拒:" + r.Reason case Pending: return "处理中" default: panic(fmt.Sprintf("未处理的支付结果:%T", r)) }}
func main() { results := []PaymentResult{Success{120}, Declined{"余额不足"}, Pending{}} for _, r := range results { fmt.Println(describe(r)) } fmt.Printf("订单状态共有 %d 种\n", orderStatusCount)}
// 输出:// 成功:收款 120 元// 被拒:余额不足// 处理中// 订单状态共有 3 种运行:go run payments.go。iota 在一个 const 块里从 0 开始,每行加 1,后面几行省略的类型和表达式沿用第一行。Go 没有办法列出一个类型的所有常量,所以惯用写法是在末尾放一个不导出的计数常量。type switch 里每个 case 中的 r 已经是对应的具体类型,可以直接读 r.Amount。接口方法 isPaymentResult 首字母小写,别的包写不出同名方法,也就实现不了这个接口。
Go 1.18 的泛型里出现过类似联合的写法 interface{ int | string },但这种类型集合(type set)只能当类型参数的约束,不能声明这种类型的变量;把它扩展成真正的和类型的提案仍处于讨论阶段。
底层怎么实现。 OrderStatus 是以 int 为底层类型的新类型,iota 在编译期展开成 0、1、2、3,运行时和普通整数没有区别。接口值占两个字长:一个指向类型信息,一个指向数据。type switch 拿接口值里的类型指针和各 case 的类型比较(分支多时按类型哈希二分查找),命中后把数据指针转成具体类型。非零大小的值装进接口时要有一块地方存放数据,逃逸到堆上就是一次分配;Pending{} 是零大小的结构体,不占数据空间。
iota常量可以被任意整数冒充:var s OrderStatus = 42合法,函数参数写成OrderStatus也挡不住OrderStatus(7);没有String()方法时打印出来只是数字。type switch漏分支不报错,go vet也不查:加了Refunded而不改describe,编译通过,运行到这里才走default并 panic;若不写default而在switch后面补一句return "",新种类就静默得到一个空串。- 封闭并不彻底:别的包可以在自己的结构体里嵌入本包的
Success,被嵌入类型的方法会被提升,于是外部类型也满足了接口;传入nil接口值同样会走到default。
把五种写法放在同一个改动面前:给支付结果加上第四种“已退款”,describe 一行不改。
从左往右看,发现问题越来越晚:只有 TS 在编译期直接拒绝;C# 对枚举给出警告,对 C# 14 的 record 层级则和 Python、JS、Go 一样,要等检查器或运行时。
| 维度 | Python | JS/TS | C# | Go |
|---|---|---|---|---|
| 支持程度 | 标准库 Enum,原生 match;和类型需模拟 |
JS 没有;TS 原生联合类型 | 原生 enum、模式匹配;联合在 C# 15 预览 |
没有,需模拟 |
| 实现机制 | 元类建单例成员 + 逐条比对的字节码 | 编译期收窄类型,运行时是普通对象 | 整数值类型 + 决策图 | 带类型的整数常量 + 接口类型比较 |
| 枚举的本质 | 每个成员是唯一对象 | JS:冻结对象;TS:生成的映射对象 | 底层整数,可强转任意值 | iota 生成的整数 |
| 和类型 | dataclass + X | Y | Z 标注 |
带 kind 字段的对象联合 |
抽象 record + 派生 record |
接口 + 不导出的方法 |
| 模式匹配 | match:类、字面量、序列、映射、守卫 |
switch 比较标签 |
switch 表达式:类型、属性、位置、关系 |
type switch |
| 穷尽性检查 | 类型检查器 + assert_never |
JS 无;TS 用 never 编译期报错 |
警告 CS8509,record 层级要写 _ |
无 |
| 加新分支时谁提醒你 | mypy、Pyright | JS 无人;TS 是 tsc | 枚举靠编译器警告,record 靠运行时 |
运行时 panic |
最大的差异在“穷尽性检查”一行:同样是“漏处理了一种情况”,TS 是编译错误,C# 是警告或运行时异常,Python 取决于有没有跑检查器,JS 和 Go 只能等它真的发生。
和类型加模式匹配的组合来自 ML 系语言。Hope、Standard ML 和后来的 Haskell、OCaml 把“数据由哪几种情况组成”作为定义类型的基本方式,编译器从一开始就知道每个类型的全部构造器,穷尽检查是顺带得到的。C 走的是另一条路:enum 只是给整数起名字,和整数可以互相赋值,带数据的变体要手写结构体加 union 加标签字段,两者对不对得上全凭自觉。C++ 和 C# 继承了 C 的 enum(Java 5 的 enum 反而是类,每个成员是单例对象);C# 的 enum 至今是整数,因为它要和 C 风格的 Windows API、位标志([Flags])、数据库里的整数列直接互通。
TS 面对的是已经存在的 JavaScript 代码:大量对象本来就用 type、kind 这样的字符串字段区分种类。TS 不能改变运行时,就在类型层面加上字面量类型和联合类型,把这种现成写法“认”成和类型,再借控制流分析做收窄和 never 检查,不需要任何运行时支持;反过来,enum 这种要生成代码的老语法与“只删类型”的工具链越来越不合拍。Python 的路线相似:match 由 PEP 634 在 3.10 加入运行时,但类型和穷尽检查留给外部检查器。
C# 是一步步把模式匹配加进来的:C# 7 加类型模式,C# 8 加 switch 表达式和属性、位置模式,C# 9 加 record、关系模式和不带变量的类型模式。每一步都要兼容开放的继承体系:任何类都可能在别的程序集里被继承,编译器无法断定“只有这几种”,穷尽检查只能对枚举、布尔值这类封闭的取值有效,而且为了不破坏旧代码只给警告。真正的可辨识联合需要新的语言概念,这就是 C# 15 的 union 和 closed。
Go 的官方 FAQ 解释过为什么没有变体类型(variant type):它们和接口的功能大面积重叠,两者放在一起会让人困惑,所以设计者选择只保留接口。于是“只有这几种”写不进类型系统,穷尽检查要靠社区的第三方静态检查工具,标准工具链不做。
这些选择的后果主要落在两处。重构:TS 里给联合加一种情况,编译器列出所有要改的地方;Python 取决于 CI 有没有跑检查器;C# 的 record 层级、JS 和 Go 只能靠搜索代码和测试。序列化:带 kind 字段的对象在 TS 和 JS 里本来就是 JSON,一行 JSON.parse 就能还原;C# 的 System.Text.Json 要在基类上标注 [JsonPolymorphic] 和 [JsonDerivedType],序列化时写入一个类型标签字段;Go 的 encoding/json 不能直接解码到接口类型,要自己先读出标签再决定解码成哪个结构体;Python 的 dataclass 也要自己约定标签字段。
旁注:别的语言
Section titled “旁注:别的语言”Rust 的 enum 就是完整的和类型:每个变体可以带不同的数据,内存里是一个标签加上“最大的那个变体”大小的空间。match 必须穷尽,漏了一个变体是编译错误,不是警告:
match r { PaymentResult::Success { amount } => format!("成功:收款 {amount} 元"), PaymentResult::Declined { reason } => format!("被拒:{reason}"),} // error[E0004]: non-exhaustive patterns: `&PaymentResult::Pending` not coveredHaskell 用一行 data PaymentResult = Success Int | Declined String | Pending 定义同样的类型。
- 情况之间只有名字不同,用枚举;每种情况带的数据不同,用和类型,不要用“一个类加几个可空字段再加一个状态字段”。
- 每处分派都写上兜底:Python 用
case _: assert_never(...),TS 用assertNever,C# 用抛UnreachableException的_,Go 用default: panic(...),JS 在default里抛错。漏掉时报错,而不是悄悄返回None或undefined。 - Python 项目要真正得到穷尽检查,就在 CI 里跑 mypy 或 Pyright;不跑,
assert_never只是一句运行时断言。 - 从外部读进来的枚举值和标签先校验:C# 用
Enum.IsDefined,Go 和iota常量比较范围,TS 和 JS 检查kind是否在已知集合里。 - 联合类型加穷尽匹配可以代替访问者模式,见设计模式书的访问者一章;类型码的重构见重构书的以子类取代类型码。
用 TS 和 C# 各写一个表示“形状”的和类型,有两种:圆(半径)和矩形(宽、高)。写一个函数按种类计算面积,对半径为 3 的圆和 4 × 5 的矩形各打印一行面积,保留两位小数:
圆的面积:28.27矩形的面积:20.00写完后,在类型里加上第三种形状“三角形”,但不改面积函数,观察两种语言的编译器分别给出什么:是错误、警告,还是什么都没有?
提示:TS 用带 kind 字段的联合和 never;C# 用抽象 record 和 switch 表达式,想一想 _ 分支写不写,两种选择下加三角形时分别会怎样。
- 枚举是有限个命名常量;和类型是“几种情况之一,每种带自己的数据”,与积类型合称代数数据类型;模式匹配按形状分派并取出数据,穷尽性检查确保每种情况都处理到。
- 枚举的本质各不相同:Python 的成员是唯一对象,C# 和 Go 的是整数,可以被任意整数冒充;TS 的
enum会生成运行时对象,Node 的类型剥离不支持它。 - 和类型在 TS 里是原生的联合类型,在 Python 里是
dataclass加类型标注,在 C# 14 里是抽象record层级,在 Go 里是接口加不导出的方法;C# 15 的union截至 2026 年 10 月仍在预览版。 - 加一种新情况时:TS 用
never在编译期报错,C# 对枚举给警告 CS8509,Python 靠类型检查器,JS 和 Go 只能等运行时的兜底分支。