当前位置:

首页 > 编程开发 > C# 15 类型系统改进Union Types详解

C# 15 类型系统改进Union Types详解

C#15引入Union联合类型,可将值限定为一组封闭类型,配合switch表达式实现编译器穷尽性检查,无需兜底分支。支持隐式转换和模式匹配自动拆包,编译器自动生成样板代码,保障类型安全。

前言

C# 的 Union 类型(联合类型),这个特性从最初提案到正式落地,中间经过了多年的打磨,现在终于在 C# 15 中与大家见面了。它的核心价值在于,能把一个值限定为一组封闭类型中的某一种,并且在 switch 表达式中获得编译器的穷尽性检查。换句话说,编译器会帮你确认是否处理了所有情况,很多时候就不需要那个烦人的 _ 兜底分支了。

C# 15 类型系统改进Union Types详解

那么,这个让社区期待已久的新特性,具体怎么用,又解决了哪些实际问题?下面来详细看看。

从一个实际问题出发

以前,假如你想写一个既能返回正常结果又能返回错误的函数,最常见的做法是什么?定义一个包装类:

public class Result
{
    public T? Data { get; set; }
    public Exception? Error { get; set; }
    public bool IsSuccess => Error is null;
}

这种写法的问题很明显:Data 和 Error 同时存在于类里,编译器无法保证“成功时一定有 Data”或“失败时一定有 Error”。整个正确性靠的是人为约定,而不是类型系统的强制保障。

而 Union 类型的出现,恰好能从根本上解决这个尴尬局面。

Union 声明

C# 15 为我们带来了一种全新的声明方式——用 union 关键字,语法有多简洁呢?看下面这行:

public union Pet(Cat, Dog, Bird);

就这一行,定义了一个名为 Pet 的联合类型,它的值可以是 Cat、Dog 或 Bird 中的任意一种。

实际上,编译器会把这个声明展开成一个结构体,内部用单个 object 引用来存储值:

// 编译器生成的等价代码
[Union] public struct Pet : IUnion
{
    public Pet(Cat value) => Value = value;
    public Pet(Dog value) => Value = value;
    public Pet(Bird value) => Value = value;
    public object? Value { get; }
}

所以它本质上就是一种简洁的结构体声明方式,编译器帮你把那些样板代码都生成了。

再来看一个很实用的例子,用 Union 和已有类型组合实现 Option:

public record class None();
public record class Some(T Value);
public union Option(None, Some);

Union 里还能加自定义方法:

public union OneOrMore(T, IEnumerable)
{
    public IEnumerable AsEnumerable() => Value switch
    {
        IEnumerable list => list,
        T value => [value],
    };
}

这体验是不是很丝滑?

另外,case 类型不限于具体类,还可以是接口、类型参数、可空类型,甚至是其他 Union,case 之间也允许重叠。

不过要注意,Union 声明是一种刻意“收紧”的形式。你可以添加方法之类的成员,但不能声明实例字段、自动属性或类字段事件;也不能自己声明 public 的单参数构造函数,而你显式添加的构造函数必须通过 this(...) 委托到编译器生成的 case 构造函数之一。这些限制是为了保证一致性。

Union 转换

Union 类型支持从每个 case 类型到联合类型的隐式转换:

Cat cat = new Cat("小花");
Pet pet = cat; // 隐式 union 转换,不需要显式构造

编译器会自动把它转成对构造函数的调用:

// 编译器实际生成的代码
Pet pet = new Pet(cat);

也就是说,你不需要手动去包装值,直接赋值就行。但有个优先级问题——如果你之前有自定义的隐式转换运算符,它的优先级会高于 union 转换,所以现有代码不会受影响。

这里还有一个容易忽略的细节:Union 转换只有隐式形式。即便某个 case 类型存在显式转换,也不代表你因此就自动拥有了到整个 Union 类型的显式转换。

Union 匹配

这才是 Union 类型真正的威力所在——和模式匹配的配合。当你对一个 Union 值进行模式匹配时,编译器会自动拆包内部的值:

Pet pet = GetPet();
if (pet is Dog dog)
{
    // dog 已经是 Dog 类型,直接使用
    dog.Bark();
}
// switch 表达式
string description = pet switch
{
    Dog dog => $"这是一只狗:{dog.Name}",
    Cat cat => $"这是一只猫:{cat.Name}",
    Bird bird => $"这是一只鸟:{bird.Name}",
};

注意最后那个分支,没有 _ 兜底!因为编译器知道 Pet 的 case 类型只有 Dog、Cat 和 Bird,所以它可以把这个 switch 表达式视为穷尽的。这不仅简化了代码,还在编译期保证了安全性。假如以后你给 Pet 增加了一个 Fish,所有没处理 Fish 的 switch 都会产生编译警告,这就不会漏了。

对于无条件的 var 和 _ 模式,匹配的是 Union 值本身而不是内部值:

if (pet is var p) { ... } // p 是 Pet 类型,不是 object

这个设计是刻意为之。var 通常只是给当前值起个名字,保留 Union 类型比解出一个 object? 更实用。

这也意味着,pet is Pet p 并不等同于 pet is var p。Pet p 这种类型模式,会对拆包后的内部值进行匹配,而不是外层的 Union 值本身,所以它通常不会成功。

null 模式里还有一个值得专门提醒的细节。对于基于 class 的 Union,result is null 在两种情况下都会成功:Union 对象本身是 null,或者它内部的 Value 是 null。对于 U? 这种“nullable 包裹 struct Union”的情况也类似:如果外层 nullable 没有值,或者内部 Union 的 Value 为 null,u is null 都会成功。而其他 Union 匹配模式只有在外层值本身存在时才会成功。

Union 穷尽性

刚才提到穷尽性检查是 Union 类型最重要的能力,这里再展开看看:

union Result(int, string, Exception);
string Describe(Result r) => r switch
{
    int n => $"数字:{n}",
    string s => $"字符串:{s}",
    Exception e => $"错误:{e.Message}",
    // 编译器认为已穷尽,无需 _ 分支
};

但有一类情况需要留意:如果 Union 的值可能为 null(比如某个 case 类型是可空的),编译器会要求你处理 null:

Pet pet = GetNullableDog(); // pet.Value 可能是 null
var result = pet switch
{
    Dog dog => "汪",
    Cat cat => "喵",
    Bird bird => "啾",
    // 警告:未处理 null
};

所以,多了解这一点能帮你避免不少坑。

手动实现 Union 模式

Union 声明虽然方便,但并不是获得 Union 行为的唯一方式。你完全可以自己动手实现,只需满足以下条件:

  1. 类型标记 [Union] 属性
  2. 提供对应每个 case 类型的单参数构造函数
  3. 提供一个 object? 类型的 Value 属性
[Union]
public struct IntOrString
{
    private readonly object _value;
    public IntOrString(int value) => _value = value;
    public IntOrString(string value) => _value = value;
    public object? Value => _value;
}

这在需要适配已有类型,或者需要自定义存储策略时非常有用。

Union 成员提供者(IUnionMembers)

默认情况下,编译器通过 Union 类型自身的构造函数来识别 case 类型。但有些场景下你可能不想暴露公开构造函数,或者想用工厂方法来创建 Union 值。这时可以在 Union 类型内部声明一个名为 IUnionMembers 的接口,让它充当成员提供者。

[Union]
public record class Result : Result.IUnionMembers
{
    object? _value;
    public interface IUnionMembers
    {
        public static Result Create(T value) => new() { _value = value };
        public static Result Create(Exception value) => new() { _value = value };
        public object? Value { get; }
    }
    object? IUnionMembers.Value => _value;
}

当 Union 类型内部包含 IUnionMembers 接口声明时,编译器就不再从 Union 类型本身查找构造函数了,而是从这个接口上的 Create 工厂方法来确定 case 类型。Value 属性也改在接口上声明。

这种模式的好处很明显:

  • 可以对外隐藏构造函数,只通过工厂方法创建实例
  • 适合 class 类型的 Union(Union 声明默认生成的是 struct)
  • 能灵活控制内部存储和初始化逻辑

使用时,隐式转换会自动走工厂方法:

Result result = "Hello";
// 等价于
Result result = Result.IUnionMembers.Create("Hello");

Non-boxing 访问模式

默认的 Union 模式通过 object? 类型的 Value 属性访问内部值,值类型会产生装箱。如果你对性能有更高要求,可以额外实现 HasValue 和 TryGetValue 方法,让编译器在模式匹配时使用强类型的访问路径:

[Union]
public struct IntOrBool
{
    private bool _isBool;
    private int _value;
    public IntOrBool(int value) => (_isBool, _value) = (false, value);
    public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);
    public object Value => _isBool ? (object)(_value == 1) : _value;
    // Non-boxing 访问模式
    public bool HasValue => true;
    public bool TryGetValue(out int value)
    {
        value = _value;
        return !_isBool;
    }
    public bool TryGetValue(out bool value)
    {
        value = _isBool && _value == 1;
        return _isBool;
    }
}

这样编译器在进行模式匹配时,就不需要通过 Value 属性来装箱取值了,而是直接调用对应的 TryGetValue,避免了装箱的开销。

Result 模式的例子

回到文章开头的问题,现在用 Union 来实现一个类型安全的 Result 就是一句话的事:

public union Result(T, Exception);

用起来是这样的:

Result Divide(int a, int b)
{
    if (b == 0) return new DivideByZeroException();
    return a / b;
}
var result = Divide(10, 3);
var message = result switch
{
    int value => $"结果是 {value}",
    Exception ex => $"出错了:{ex.Message}",
};

不需要额外的包装类,不需要 IsSuccess 属性,类型系统保证了每种情况都被处理。和以前的做法比起来,确实优雅得多。

Union 与类型层次结构

值得注意的是,C# 的 Union 类型是“类型的联合”而不是“带标签的联合”。如果你需要更接近传统 discriminated unions 的效果(即每个分支有独立的名称和数据),可以用 record 作为 case 类型来组合:

public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
public record class Triangle(double Base, double Height);
public union Shape(Circle, Rectangle, Triangle);
double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t => 0.5 * t.Base * t.Height,
};

如果不需要命名的分支,直接用现有类型组合就好。两种方式各有适用场景,C# 在这个设计上给了充分的灵活性。

另外,如果你需要更加严格的封闭类型层次结构,还可以关注即将推出的 closed hierarchies 特性,它和 Union 类型是互补的关系。

为什么不用类型擦除?

看到这里你可能会好奇:Union 值在运行时不就是一个 object 引用吗?那为什么不直接用 object 加上编译期的元数据信息来表示 Union 类型呢?

这个思路在 C# 语言设计工作组中确实被认真讨论过,但最终没有成为 C# 15 的实现方案。原因有几个:

泛型场景下会破坏类型安全。 考虑这样一段代码:

public class MyCollection
{
    public bool TryAdd(object o)
    {
        if (o is T t)
        {
            // 添加 t
            return true;
        }
        return false;
    }
}

如果用 MyCollection<(int or string)> 实例化,而 (int or string) 被擦除成了 object,那么 o is T 就变成了 o is object,永远成功。任何类型的值都能绕过检查被塞进集合,类型安全就崩了。

不擦除也有问题。 如果换成包装类型 ValueUnion 来避免擦除,那 (string or bool) 和 (bool or string) 在运行时就是不同的类型。这对 ad hoc union 来说是无法接受的,用户自然会认为这两个同一组类型的联合应该是可以互换的。

包装类型方案在实用性上胜出。 语言设计工作组整理过一份 Trade Off Matrix,对比了三种路线:类层次结构、object 引用(擦除)和包装类型。最终选择的包装类型方案(即现在的 Nominal Type Unions)在向后兼容性、非 ABI 破坏性、可定制实现以及交付周期等维度上都有明显优势。擦除方案虽然在匿名语法和动态模式匹配方面更出色,但需要较大的运行时改造才能安全工作,短期内无法落地。

所以最终的设计是:Union 声明生成一个结构体包装,内部用 object? 引用存值。你可以把它理解为一个编译器帮你维护的包装类型,但它不是单纯的元数据注解——运行时确实存在这个结构体,Value 属性也是真实可访问的。这让 Union 在反射、序列化、跨程序集调用等场景下都能正确工作,而不只是一个编译器的错觉。

结语

Union 类型的加入,是 C# 类型系统一次质的飞跃。它解决了长期以来用 C# 表达“多选一”类型时的尴尬:不再需要靠约定、靠运行时检查,而是让编译器从类型层面帮你把关。简洁的 union 声明语法让大多数场景几行代码就搞定,而灵活的 Union 模式又允许在需要时完全自定义底层实现。这种简单场景简单做,复杂场景有出路的设计理念,非常符合 C# 一向的风格。

期待 C# 15 和 .NET 11 的正式发布。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
codex安装windows教程桌面版完整安装步骤指南
codex安装windows教程桌面版完整安装步骤指南

详解如何在 Windows 电脑上通过 Microsoft Store 安装 Codex 桌面应用,包括启动登录、切换 Codex 模式、关联本地代码项目及终端环境配置的界面操作指引。

github配置ssh key windows
github配置ssh key windows

本文详解如何在Windows环境中使用PowerShell生成SSH密钥对,配置ssh-agent自动加载私钥,并将公钥安全添加至GitHub账户,最后通过git remote set-url切换为SSH地址,解决每次操作需输入账号密码的问题。

backdrop和background区别
backdrop和background区别

深入解析backdrop与background在网页开发及视觉设计中的区别。通过CSS代码示例,展示如何正确使用background设置元素背景,以及利用backdrop-filter创建弹窗遮罩模糊效果,避免概念混用。

windsurf ide download Windows版安装教程
windsurf ide download Windows版安装教程

详解 Windsurf IDE 在 Windows 系统的下载来源、安装步骤及首次启动界面。指导用户如何导入编辑器设置、打开项目文件夹及使用终端与 AI 功能,适合初次接触该工具的开发者阅读。

ServBay安装配置详细教程与操作指南
ServBay安装配置详细教程与操作指南

新手入门 ServBay 本地开发环境,详解安装包下载、Dashboard 状态监控、Packages 组件安装、Services 服务控制及 Websites 项目配置。掌握 .servbay.config 版本管理与日志排查技巧,快速搭建稳定的 PHP、Node.js 等多语言开发环境。

codekit环境配置指南从安装到环境搭建完整教程
codekit环境配置指南从安装到环境搭建完整教程

详解 CodeKit 在 macOS 下的安装步骤、项目导入方法、Sass与JavaScript编译设置及浏览器自动刷新功能,助您快速搭建高效的前端开发环境。

codex安装windows 命令行完整操作教程
codex安装windows 命令行完整操作教程

详解Windows环境下安装OpenAI Codex CLI的步骤,包括WSL环境检查、Node.js/npm配置、npm全局安装命令及首次启动验证,适合开发者快速上手。

NativeRest环境配置要求与完整操作教程
NativeRest环境配置要求与完整操作教程

学习如何配置 NativeRest REST API 客户端。涵盖 Windows/macOS/Linux 安装后的工作区创建、环境变量管理、请求编辑及响应查看步骤,帮助开发者快速完成基础环境搭建与连通性测试。

CSS设置透明度的注意事项有哪些?opacity属性详解
CSS设置透明度的注意事项有哪些?opacity属性详解

深入解析CSS中设置透明度的核心属性opacity,剖析子元素继承、事件穿透、层叠上下文等关键注意事项,并提供与rgba、hsla的实用选型对比。

flutter页面传值到后台的方法及示例代码
flutter页面传值到后台的方法及示例代码

flutter页面传值到后台的完整实现方法及示例代码,帮助读者快速掌握相关技术要点。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
Shapr3D macOS版
Shapr3D macOS版
Mac

Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。

REAPER macOS版
REAPER macOS版
Mac

REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

Ableton Live macOS版
Ableton Live macOS版
Mac

Ableton Live 是面向音乐制作人与现场表演者的数字音频工作站,提供编曲视图、独具特色的现场视图、音频录制、MIDI创作、实时变速、乐器及效果器。Mac版原生支持Apple芯片,并可连接音频接口、MIDI控制器和第三方插件。

WINDOWS 更多
3dmax(3ds max)
3dmax(3ds max)
Windows

Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

photoshop
photoshop
Windows、macOS 、 iPad

Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

Blender
Blender
Windows、macOS 和 Linux

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。