跳转到内容

第 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 的标准库有 enum 模块(3.4 起);3.10 起有 match 语句做结构模式匹配(structural pattern matching);和类型没有专门语法,用几个 dataclass 加类型标注里的联合 | 表示。穷尽检查不在解释器里,只在 mypy、Pyright 这类类型检查器里。

from dataclasses import dataclass
from enum import Enum
from 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 只把“漏了”变成运行时异常,写代码时提醒你的只有检查器。

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# 有原生的 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 没有枚举,也没有和类型。替代办法是 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 也要自己约定标签字段。

Rust 的 enum 就是完整的和类型:每个变体可以带不同的数据,内存里是一个标签加上“最大的那个变体”大小的空间。match 必须穷尽,漏了一个变体是编译错误,不是警告:

match r {
PaymentResult::Success { amount } => format!("成功:收款 {amount} 元"),
PaymentResult::Declined { reason } => format!("被拒:{reason}"),
} // error[E0004]: non-exhaustive patterns: `&PaymentResult::Pending` not covered

Haskell 用一行 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 只能等运行时的兜底分支。