C# 15 正式引入了联合类型(Union Types),这是语言设计者长期以来的重点提案,也是代数数据类型(ADT)在 C# 中的首次原生支持。联合类型允许开发者声明一个封闭的类型集合,编译器能够确保所有可能的 case 都被完整处理,从而将运行时错误转化为编译时警告。本文将从语法特性、穷尽性匹配机制以及工程设计选择三个维度,系统性分析这一新特性的设计理念与实践要点。

联合类型的基本语法与类型声明

在 C# 15 之前,当方法需要返回多种可能类型时,开发者通常依赖 object 类型、标记接口或抽象基类。这些方案存在根本性缺陷:object 放弃类型约束,接口和基类无法封闭 —— 任何人都可以派生新类型,编译器无法确认所有可能的分支已被覆盖。联合类型正是为解决这一问题而设计。

联合类型的声明简洁直观,使用 union 关键字定义一个封闭的类型集合。以下是一个典型的宠物类型声明示例:

public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);

public union Pet(Cat, Dog, Bird);

这一行代码声明了 Pet 作为一种新类型,其值可以是 CatDogBird 三者之一。编译器自动为每个 case 类型生成隐式转换,开发者可以直接赋值而无需显式构造:

Pet pet = new Dog("Rex");
Pet pet2 = new Cat("Whiskers");

关键约束在于:只有声明中的 case 类型才能赋值给联合类型变量,编译器会阻止任何非法的类型转换。这种封闭性是实现穷尽性匹配的基础。

穷尽性模式匹配的编译时保障

联合类型的核心价值在于编译器能够验证 switch 表达式是否覆盖了所有 case。当一个联合类型的实例已知非空时,编译器要求处理每一个 case 分支,否则将产生编译错误而非运行时异常:

string name = pet switch
{
    Dog d => d.Name,
    Cat c => c.Name,
    Bird b => b.Name,
};

这段代码无需 default 分支或 discard 模式 _,因为编译器能够确认 pet 必然是三者之一。如果后续在联合声明中添加第四个 case 类型(如 Fish),所有未更新处理逻辑的 switch 表达式都会触发编译警告。这一机制将「漏掉分支」的 bug 从运行时转移到编译阶段捕获。

需要注意的是,如果任一 case 类型是可空的(如 int?Bird?),则所有 switch 表达式必须包含 null 分支以满足穷尽性要求。联合类型的 Value 属性默认状态为「可能为空」(maybe-null),这是语言设计者为数组和默认值场景提供正确空状态分析的必要折中。

联合类型还支持在声明体内添加成员方法,实现一站式的数据处理逻辑。例如一个 OneOrMore<T> 类型可以同时包含单一值和集合两种 case,并在内部提供 AsEnumerable() 方法进行统一:

public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        T single => [single],
        IEnumerable<T> multiple => multiple,
        null => []
    };
}

密封层次结构与联合类型的协同设计

联合类型并非孤立的特性,而是 C# 完善穷尽性类型系统的一部分。C# 15 同时引入了两个相关提案:封闭层次结构(Closed Hierarchies)和封闭枚举(Closed Enums)。封闭层次结构使用 closed 修饰符阻止程序集外部的派生类,封闭枚举则防止创建枚举声明之外的值。三者共同构成了完整的穷尽性匹配体系。

在工程实践中,联合类型与传统密封类层次结构之间存在选择考量。密封层次结构适用于需要共享行为和状态的场景 —— 基类可以定义抽象方法,各派生类实现具体逻辑。而联合类型更适合「数据联合」场景:case 之间无需共享行为,仅在匹配时提取数据。此外,联合类型能够组合完全无关的类型(如 stringException),这是继承体系无法表达的能力。

对于已有代码库,迁移策略值得审慎规划。现有使用抽象基类加 sealed 派生类的模式可以逐步迁移至联合类型,但需要评估是否需要跨程序集的扩展性 —— 联合类型的封闭性意味着无法在外部程序集添加新 case,这与密封层次结构的限制一致。

工程实践中的性能与兼容性考量

联合类型在底层实现上生成为一个结构体,包含一个 object? 类型的 Value 属性存储实际值。值类型会被装箱处理,这对大多数场景是可接受的简化。但对于性能敏感的场景,C# 15 提供了自定义联合类型的扩展机制:带有 [System.Runtime.CompilerServices.Union] 标记的类型,只要遵循特定的构造模式(单一参数构造函数加公共 Value 属性),即可被编译器识别为联合类型。

更高级的自定义实现可以避免装箱:通过添加 HasValue 属性和 TryGetValue 方法,编译器能够在模式匹配时直接访问底层值,而非通过 Value 属性进行装箱。这一机制使得库作者能够针对特定值类型组合优化存储布局。

目前联合类型处于预览阶段,需要 .NET 11 Preview 2 SDK 并在项目中设置 <LangVersion>preview</LangVersion>。由于运行时尚未包含 UnionAttributeIUnion 接口,开发者需要在项目中手动声明这些类型。微软鼓励社区反馈以影响最终设计方向,这一特性预计将在后续预览版本中逐步完善。

结语

C# 15 联合类型为语言带来了代数数据类型的原生支持,使开发者能够在编译期确保所有分支被处理。配合封闭层次结构和封闭枚举,C# 正构建一个完整的穷尽性类型系统。对于需要建模有限状态、处理多返回值或表达互斥数据的场景,联合类型提供了比传统继承更安全、更表达力更强的选择。工程团队在引入这一特性时,应评估其与现有类型层次结构的兼容性,并在性能敏感路径上考虑自定义实现方案。

资料来源:本文技术细节参考 Microsoft .NET 官方博客(C# 15 Union Types, 2026-04-02)及 C# 语言规范联合类型提案文档。