.NET 11 预览版 2 引入联合类型:C# 15 新特性解析与应用指南!

发布时间:2026/5/24 10:10:14

.NET 11 预览版 2 引入联合类型:C# 15 新特性解析与应用指南! 这是系列文章[探索 .NET 11 预览版](/series/exploring-the-dotnet-11-preview/) 的第二篇。1. [第1部分 - 使用 Web Workers 在 Blazor 中运行后台任务](/exploring-the-dotnet-11-preview-1-running-background-tasks-in-blazor-with-web-workers/)2. 第2部分 - .NET好吧C#终于有联合类型了本文 。联合类型是多年来一直被期待的特性之一在 .NET 11更确切地说是 C# 15中它们 _终于_ 来了。下面将介绍这种支持的具体形式、如何使用它们、它们是如何实现的以及如何实现自定义类型。需注意本文是基于 .NET 11 预览版 4 的可用特性撰写的。从现在到 .NET 11 正式发布很多内容可能会发生变化。什么是联合类型联合类型是函数式编程领域中常用的基本数据结构之一在 F#、TypeScript、Rust 等几乎所有以函数式编程为主的语言中都能找到。其核心是允许一个类型表示两种不同的事物。一些最简单的联合类型是 Option 和 Result 类型。虽无“标准”版本但自定义实现非常常见。Result 有两种状态成功Result 对象包含一个 TSuccess 值表示操作成功的“成功”结果错误Result 对象包含一个 TError 值表示操作失败的“错误”结果。从方法中返回一个 Result 对象调用者必须 _明确_ 处理这两种情况而非假设操作会成功。这种模式通常被称为结果模式在 C# 中它有优点也有缺点。作者写过一系列关于使用这种模式的文章[还探讨了它是否值得使用](/series/working-with-the-result-pattern/)。不过联合类型不一定是这种超级通用的形式它们可以用来表示任意组合的类型集合。C# 15 中使用 union 关键字的联合类型上一节以经典的 Result 类型为例介绍了联合类型但其用途远不止于此。当需要处理可能是几种潜在不相关类型之一的数据时它们是理想的选择。例如有三种不同的 record 类型它们包含不同的属性代表不同的操作系统csharp public record Windows(string Version); public record Linux(string Distro, string Version); public record MacOS(string Name, int Version); 这些类型 _没有_ 共同的值。在 C# 15 之前处理可能是 Windows、Linux 或 MacOS 对象的主要方法有尝试创建一个基类让所有类型都从该基类派生。但如果无法控制这些类型因为它们来自某个库则不可行将类型存储在 object 实例中。虽可行但会失去类型安全使用某种“标签”值来跟踪对象包含的类型例如使用 enum 来跟踪。在 C# 15 中可使用 union 关键字直接支持这种场景如下所示csharp // 使用 union 作为类型 public union SupportedOS(Windows, Linux, MacOS); // 列出属于联合的类型 可以通过以下几种方式创建 SupportedOS 类型的实例csharp // 可以调用 new 并传入一个实例 SupportedOS os new SupportedOS(new MacOS(Tahoe, 25)); // 或者可以使用隐式转换实际上在幕后调用了 new() SupportedOS os new MacOS(Tahoe, 25); 生成的 union 类型实现了 IUnion 接口csharp public interface IUnion { object? Value { get; } } 因此若需要可以始终将“内部”的实例值作为 object? 取出来csharp // 可以使用 .Value 访问存储的“内部”对象 Console.WriteLine(os.Value); // MacOS { Name Tahoe, Version 25 } 处理联合类型的标准方法是使用 switch 表达式csharp string GetDescription(SupportedOS os) os switch { Windows windows $Windows {windows.Version}, Linux linux ${linux.Distro} {linux.Version}, MacOS macOS $MacOS {macOS.Name} ({macOS.Version}), }; // 注意不需要 _ 通配符 switch 表达式会自动提取内部的实例类型且不需要包含 _ “通配”情况编译器会强制要求检查所有允许的值且 _只_ 需要检查这些值。若遗漏某个值会收到警告plaintext 警告 CS8509: 开关表达式未处理其输入类型的所有可能值它不是详尽的。例如未涵盖模式 MacOS。 注意如果某个实例类型是可空的例如 MacOS?那么还需要在 switch 表达式中处理 null。回到最初的话题可以将 Result 类型实现如下这只是一个示例有很多不同的实现方式csharp public union Result(T, Exception); 或者展示另一个经典的 Option 类型csharp public record class None; public union Option(None, T); 以上就是 C# 15 中 union 类型的基础知识接下来将看看如何在今天使用它们然后深入了解它们的实现原理。在 .NET 11 中使用 union 类型要使用 union 类型需要做两件事安装 .NET 11 预览版 2 及以上版本的 SDK。最初的 union 支持是在预览版 2 中添加的但安装预览版 4 及以上版本体验会更流畅在 .csproj 文件中启用预览语言支持方法是添加 preview xml Exe preview net11.0;net8.0;net48 enable enable 需注意虽然需要使用 .NET 11 SDK但可以针对早期版本的运行时就像在上面的 .csproj 文件中所做的那样。union 支持是作为编译器特性实现的因此它在早期运行时上也可用[即使从技术上讲它们并不支持](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-versioning)。但是如果针对的是早期运行时或者使用的是 .NET 11 预览版 2 或预览版 3那么还需要在项目中添加一些辅助类型csharp #if !NET11_0_OR_GREATER namespace System.Runtime.CompilerServices; [AttributeUsage(Class | Struct, AllowMultiple false, Inherited false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } [这些类型](https://github.com/dotnet/runtime/pull/127001) 在 .NET 11 预览版 4 中被添加因此如果使用的是较新的 SDK它们会自动可用但无论如何若针对的是早期运行时就需要包含它们。正如可能已经猜到的当编译器创建 union 类型时它会使用这个属性并实现这个接口。接下来将看看生成的代码是什么样的以了解 union 类型是如何实现的。在 IDE 支持方面若使用的是 Visual Studio 预览版或 VS Code 的 C# DevKit 内部版本那么应该会有初步的支持。[JetBrains Rider 的支持仍在等待中](https://youtrack.jetbrains.com/projects/RIDER/issues/RIDER-135866/ETA-for-net11-preview-1-support)。union 类型是如何实现的可以在 [这里](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/unions) 查看 union 类型的完整规范标准的生成代码其实非常简单csharp using System.Runtime.CompilerServices; [Union] public struct SupportedOS : IUnion { public object? Value { get; } // 每个实例类型的构造函数 public SupportedOS(Windows value) this.Value (object) value; public SupportedOS(Linux value) this.Value (object) value; public SupportedOS(MacOS value) this.Value (object) value; } 如所见生成的 SupportedOS 类型是一个 struct用 [Union] 属性修饰有一个只读的 object? Value 属性实现了 IUnion 接口为它支持的每个实例类型都有一个构造函数。有点惊讶地发现从实例类型到 SupportedOS 类型没有隐式转换尽管可以编写这样的代码csharp SupportedOS os new MacOS(Tahoe, 25); 不过看起来编译器只是将其重写为使用 [Union] 构造函数csharp // SupportedOS os new MacOS(Tahoe, 25); // 编译器生成的代码看起来像这样 SupportedOS os new SupportedOS(new MacOS(Tahoe, 25)); 这种隐式转换完全由 [Union] 属性驱动。如果重写示例不使用 union 关键字而是使用前面展示的实现代码但“忘记”包含 [Union] 属性就可以看到这一点csharp using System.Runtime.CompilerServices; SupportedOS os new MacOS(Tahoe, 25); // 无法将类型 MacOS 隐式转换为 SupportedOS var description os switch { Windows windows $Windows {windows.Version}, // 类型 SupportedOS 的表达式不能由类型 Windows 的模式处理 Linux linux ${linux.Distro} {linux.Version}, // 类型 SupportedOS 的表达式不能由类型 Linux 的模式处理 MacOS macOS $MacOS {macOS.Name} ({macOS.Version}), // 类型 SupportedOS 的表达式不能由类型 MacOS 的模式处理 }; public record Windows(string Version); public record Linux(string Distro, string Version); public record MacOS(string Name, int Version); // 要成为有效的联合类型这个属性是必需的 // 这里只是为了演示而移除 // [Union] public struct SupportedOS : IUnion { public object? Value { get; } public SupportedOS(Windows value) this.Value (object) value; public SupportedOS(Linux value) this.Value (object) value; public SupportedOS(MacOS value) this.Value (object) value; } 上述代码无法编译并会出现以下错误这表明 [Union] 属性驱动了隐式转换和 switch 表达式plaintext 错误 CS0029: 无法将类型 MacOS 隐式转换为 SupportedOS 错误 CS8121: 类型 SupportedOS 的表达式不能由类型 Windows 的模式处理。 错误 CS8121: 类型 SupportedOS 的表达式不能由类型 Linux 的模式处理。 错误 CS8121: 类型 SupportedOS 的表达式不能由类型 MacOS 的模式处理。 如果恢复 [Union] 属性一切都能正常编译和运行这展示了如何创建自己的 _自定义_ 联合类型。通过自定义联合实现避免装箱既然刚开始支持 union 类型为什么可能想要创建 _自定义_ Union 类型呢一个原因是可能 _已经_ 在使用自定义联合类型比如 [OneOf](https://www.nuget.org/packages/OneOf) 或 [Sasa](https://www.nuget.org/packages/Sasa)作者过去使用过的两个包提供的类型。在这些情况下这些库可以通过简单地实现 IUnion 接口并添加 [Union] 属性来受益于内置的语言支持例如 switch 表达式支持。另一种情况是“将实例类型存储在 object 实例中”不够好。生成的联合类型 _始终_ 是一个带有单个 object 字段的 struct。这意味着如果创建一个包含多个 struct 类型的 union这些类型将被装箱到堆上。例如假设需要一个 union它可以表示 int 或 boolcsharp public union IntOrBool(int, bool); 问题在于传递给 IntOrBool 构造函数的 int 或 bool 会立即被装箱为 object并存储在 Value 属性中csharp [Union] public struct IntOrBool : IUnion { public object? Value { get; } // 结构体参数总是被装箱在堆上分配内存 public IntOrBool(int value) this.Value (object) value; public IntOrBool(bool value) this.Value (object) value; } 这会在堆上分配内存通常是不可取的因为联合类型在性能方面应该基本是透明的。使用这种实现的任何 switch 表达式同样会使用 Value 属性。例如对于基本的内置 union 实现以下表达式csharp IntOrBool intOrBool; var description intOrBool switch { int i integer, bool b bool, }; 会被转换为类似以下的代码csharp IntOrBool unmatchedValue new IntOrBool(23); object obj unmatchedValue.Value; // 访问装箱的值 string str; if (obj is int _) { str integer; } else if (obj is bool _) { str bool; } else { ThrowSwitchExpressionException((object) unmatchedValue); // 这种情况不会发生但还是要处理 } 在很多情况下装箱分配可能并不重要但在其他地方比如在热点代码路径中装箱是不可取的。为了解决这个问题union 特性允许使用 TryGetValue 模式进行“非装箱”实现。这要求实现bool HasValue { get; }如果存储的值非 null则返回 true为每个实例类型 T 实现 bool TryGetValue(out T value)。例如以下是上述 IntOrBool 类型的 [一种可能实现](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/unions#examples-of-union-types)避免了装箱csharp [Union] public struct IntOrBool : IUnion { private readonly bool _isBool; private readonly int _value; public IntOrBool(int value) { _isBool false; _value value; } public IntOrBool(bool value) { _isBool true; _value value ? 1 : 0; } public bool HasValue true; // 值永远不会为 null public bool TryGetValue(out int value) // 获取 int 值而不装箱 { value _value; return !_isBool; } public bool TryGetValue(out bool value) // 获取 bool 值而不装箱 { value _isBool _value is 1; return _isBool; } // 必须实现这个以满足 IUnion 接口 // 并且它仍然会装箱但默认情况下不会使用。 public object Value _isBool ? _value is 1 : _value; } 当实现 TryGetValue() 方法时编译器会在 switch 表达式中自动使用它们而不是 Value 属性因此上面的 switch 表达式会变成以下形式csharp IntOrBool unmatchedValue new IntOrBool(23); string str; // 调用 TryGetValue 而不是使用装箱的 Value 属性 if (unmatchedValue.TryGetValue(out int _)) { str integer; } else if (unmatchedValue.TryGetValue(out bool _)) { str bool; } else { ThrowSwitchExpressionException((object) unmatchedValue); // 这种情况不会发生但还是要处理 } 根据代码路径和用例创建这样的自定义非装箱实现可能值得也可能不值得这取决于在代码库中如何使用 union类型。还有哪些特性即将推出目前发布的 union 实现已经可以使用但 [语言提案](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/unions) 中还有更多内容尚未介绍。以下是一些即将推出的相关特性[联合成员提供程序](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/unions#union-member-providers)。这些提供了一种在与联合本身不同的类型上定义联合类型成员的方法[封闭枚举](https://github.com/dotnet/csharplang/blob/main/proposals/closed-enums.md)。这些是 enum在 enum 的 switch 表达式中 _不需要_ 包含“通配”表达式 (_ )[封闭层次结构](https://github.com/dotnet/csharplang/blob/main/proposals/closed-hierarchies.md)。这允许在 class 上添加 closed 修饰符以防止在定义程序集之外声明派生类从而同样允许在没有通配表达式的情况下进行详尽的 switch 表达式。这些特性可能会也可能不会进入 .NET 11但如果它们进入了作者一定会进行介绍总结本文介绍了 .NET 11 预览版 2 中引入的联合类型支持。描述了实现它们所需的步骤以及如何使用 switch 表达式解构联合类型。展示了 union 声明语法、它们在幕后的实现方式以及如何实现联合类型的非装箱版本。最后讨论了联合类型的一些计划和路线图以及 C# 中尚未发布的详尽性改进。

相关新闻