联合类型表示可以是多个事例类型之一的值。 联合提供每种事例类型的隐式转换、详尽的模式匹配和增强的可为空性跟踪。 使用 union 关键字声明联合类型:
public union Pet(Cat, Dog, Bird);
此声明创建一个包含三种 Pet 事例类型的联合: Cat、 Dog和 Bird。 可以将任何大小写类型值 Pet 分配给变量。 编译器确保 switch 表达式涵盖所有事例类型。
C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。
本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。
小窍门
若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。
当值必须正好是一组固定类型之一时声明联合,并且你希望编译器强制处理每种可能性。 常见方案包括:
-
结果或错误返回:方法返回成功值或错误值,调用方必须同时处理这两者。 一个联合,如
union Result(Success, Error)使结果集明确。 -
消息或命令调度:系统处理一组关闭的消息类型。 联合可确保新消息类型在尚未处理这些消息的每个
switch类型上生成编译时警告。 - 替换标记接口或抽象基类:如果只使用接口或抽象类来分组类型进行模式匹配,则联合将为你提供详尽检查,而无需继承或共享成员。
联合不同于其他类型声明的重要方式:
- 与或
classstruct不同的是,联合不会定义新的数据成员。 而是将现有类型组合到一组封闭的替代项中。 - 与关闭
interface的联合不同,在声明中定义事例类型的完整列表,编译器使用该列表进行详尽检查。 - 与 a
record不同,联合不会添加相等性、克隆或析构行为。 联合侧重于“哪一种情况?”,而不是“它有什么字段?”
联合类型依赖于UnionAttribute命名空间中的IUnion类型和System.Runtime.CompilerServices类型。 .NET 11 运行时包括这些类型。
联合声明
联合声明指定事例类型的名称和列表:
public union Pet(Cat, Dog, Bird);
事例类型 可以是转换为 object的任何类型,包括类、结构、接口、类型参数、可为 null 的类型和其他联合。 以下示例显示了不同的事例类型可能性:
public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
当事例类型是值类型(例如 int),联合声明将装箱该值并将其存储为单个 object? 引用。
自定义联合类型 可以使用不同的存储策略。
联合声明可以包含包含其他成员的正文,就像结构一样,但存在一些限制。 联合声明不能包括实例字段、自动属性或类似字段的事件。 也不能使用单个参数声明公共构造函数,因为编译器将这些构造函数生成为联合创建成员。 声明的任何构造函数都必须使用 this(...) 初始值设定项将直接或间接委托给生成的构造函数。 以下 Length 联合添加一个 TotalMeters 属性,该属性使用模式匹配来处理每个事例类型,以及组合两个 Add 长度的方法:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
联合转换
存在从每个事例类型到联合类型的隐式 联合转换 :
static void BasicConversion()
{
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
联合转换的工作原理是调用相应的生成的构造函数。 如果同一类型存在用户定义的隐式转换运算符,则用户定义的运算符优先于联合转换。 如果多个事例类型同样适用于源值,则联合转换不明确,编译器报告错误。 有关转换优先级的详细信息,请参阅 功能规范。
联合转换为可以为 null 的联合结构(T?)也适用于 T 联合类型:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy", "Beagle");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
联合模式匹配
在联合表达式上使用类型或声明模式时,该模式会测试联合的内容,而不是联合实例。 例如, Dog 模式测试是否 Value 为 Dog:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
模式的窗体确定它测试的值:
| 图案 | 联合表达式上的行为 |
|---|---|
类型或声明模式,例如 Dog 或 Dog dog |
针对指定类型测试内容。 |
具有类型的属性或位置模式,例如 Dog { Breed: "Labrador" } |
针对该类型测试内容,然后将嵌套模式应用于该事例值。 |
没有类型的属性或位置模式,例如 { Name: "Rex" } |
使用联合实例的属性或 Deconstruct 方法。 |
| 列表模式 | 使用联合实例的计数和索引成员,而不是其内容的计数和索引成员。 |
var 或放弃 _ 模式 |
捕获或匹配传入的联合实例,而不将其解包。 |
| 常量或关系模式 | 测试内容。 该 null 模式还处理 null 联合引用或可为 null 包装器。 |
例如,使用委托给包含Cat或Dog的只读Name属性定义一个Pet联合。 属性表达式 switch 中的类型模式测试内容:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
类型化模式 Dog { Breed: "Labrador" } 测试内容并读取狗的内容 Breed,这是联合没有的属性。 非类型化模式 { Name: "Rex" } 改为读取联合的属性,因此它还与名为 Rex 的 Name 猫匹配:
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" }); // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" }); // output: True
该 not 模式否定其嵌套模式。 在 pet is not null中,嵌套 null 模式检查内容,因此 not null 非常有用的 has-a-value 测试。 相比之下,空非类型化属性模式 { }检查联合实例。 它始终与不可取消的结构联合匹配,即使 Value 为 null:
Pet empty = default;
Console.WriteLine(empty is { }); // output: True
Console.WriteLine(empty is not null); // output: False
if (empty is var captured)
{
Console.WriteLine(captured.Value is null); // output: True
}
逻辑模式保留这些区别。 在 var pet and not null中, var pet 捕获传入的联合,并 not null 测试它是否具有非 null 内容。 在此 not null and var pet模式中,该 var 模式仍捕获传入的联合: not 不会更改传递给右侧模式的值。 相比之下,类型 Dog and { Breed: "Labrador" }模式将匹配 Dog 传递给属性模式。
注释
类型模式必须与至少一种大小写类型兼容。
Pet对于本文中的声明,pet is Pet生成编译器错误 CS8121:其事例类型都不匹配Pet。 它不是联合实例的运行时测试。 若要检查内容,请使用 pet is not null 或 pet.Value is not null;捕获联合本身,请使用 var 模式。
简言之,类型和声明模式测试内容。 具有类型测试内容的递归模式;不带类型的递归模式使用联合自己的成员。 这些规则在表达式和switch表达式中is是相同的。
Null 匹配
对于结构联合,模式 null 检查是否 Value 为 null:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
对于基于类的联合, null 当联合引用本身为 null 或其 Value 属性为 null 时,会成功:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
对于可以为 null 的联合结构类型(Pet?), null 当可为 null 的包装器没有值或基础联合为 Value null 时成功。
联合详尽性
switch当表达式处理每个事例类型时,表达式会详尽地处理联合事例类型。 如果可为 null 分析还认为内容为非 null,则不需要放弃(_)或 var arm:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
若要处理 null 内容,首选 null 手臂而不是 var 放弃 (_) 臂。 该 null 模式指示意向,并在添加新大小写类型时保留警告,而无需匹配臂。 或 var 放弃臂也接受任何新情况,因此编译器无法警告你该缺失的情况。
当跟踪的 Value null 状态为“可能为 null”时,如果省略 null arm,编译器也会发出警告。 例如, NullablePet 具有可为 null 的事例类型,因此其参数的内容可能为 null:
public union NullablePet(Cat?, Dog, Bird);
static string DescribeNullableCase(NullablePet pet) => pet switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
Bird bird => bird.Name,
null => "no pet",
};
从此交换机中删除 null 臂会产生警告 CS8655。
可为 Null 的分析不保证生成的联合在运行时具有非 null 内容。 默认联合包含 null,包括当它是默认初始化的字段或数组元素时。 如果所有事例类型都是不可取消的,则默认跟踪状态 Value 为“不为 null”,因此编译器不一定警告省略的 null 手臂。
null每当默认内容到达开关时处理正确性:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
对于此示例 Pet ,删除 null arm 不会生成 missing-null-arm 警告,除非前面的代码更改跟踪的 null 状态,但开关在收到默认值时会引发 SwitchExpressionException 。 以前的 is null 测试可以更改跟踪的状态并引发警告。 此行为与明确赋值分开:编译器拒绝读取未分配的局部变量。
可空性
编译器通过以下规则跟踪 union 属性的 Value null 状态:
- 如果任何事例类型的默认 null 状态为“可能为 null”,则联合
Value属性的默认 null 状态为“可能为 null”。否则,默认的 null 状态为“非 null”。 - 从事例类型(通过构造函数或联合转换)创建联合值时,
Value获取传入值的 null 状态。 - 当非装箱访问模式
HasValue或TryGetValue(...)成员查询联合的内容时,分支上的Valuenull 状态true将变为“非 null”。
自定义联合类型
编译器将 union 声明转换为 struct 声明。 结构标有 [System.Runtime.CompilerServices.Union] 属性并实现 IUnion 接口。 它包括每种事例类型和 Value 属性的公共构造函数。 该语言提供调用这些构造函数的隐式联合转换。 生成的形式是有意见的。 它始终是一个结构,始终装箱值类型的事例,并且始终将内容存储为 object?。
如果要调整现有类型、创建基于类的联合或使用自定义存储策略,或者需要互操作支持,则可能需要不同的行为。 可以手动创建联合类型。
如果属性遵循[Union],则任何具有属性的类或结构都是联合类型。 基本联合模式需要:
-
[Union]类型的属性。 - 一个或多个公共构造函数,每个构造函数都有一个按值或
in参数。 每个构造函数的参数类型定义 大小写类型。 - 具有访问器类型的
Value公共object?属性(或object)。get
上述所有工会成员都必须是公开的。 编译器使用这些成员来实现联合转换、模式匹配和详尽性检查。 还可以实现 非装箱访问模式 或创建 基于类的联合类型。 自定义联合类型可以添加更多成员。
编译器假定自定义联合类型满足以下行为规则:
-
声音:
Value始终返回null或事例类型之一的值,包括默认结构值。 与默认生成的联合的 null 内容不同,自定义联合可以将其默认值表示为非 null 大小写。 -
稳定性:如果从事例类型创建联合值,
Value则与该事例类型匹配(或者null输入为null) 。 - 创建等效性:如果值隐式转换为两种不同的事例类型,则两个创建成员都生成相同的可观察行为。
-
访问模式一致性:如果存在,则
HasValue成员TryGetValue的行为等同于直接检查Value。
以下示例显示了自定义联合类型:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
非装箱访问模式
自定义联合类型可以选择实现 非装箱访问模式 ,以便在模式匹配期间启用对值类型事例的强类型访问,而无需装箱。 此模式需要:
- 一个
HasValue类型属性booltrueValue,该null属性返回的不是 。 -
TryGetValue返回值并通过参数传递值的bool每种事例类型的out方法。TryGetValuetrue仅当该事例类型的非 null 值时Value返回。 参数out的类型可转换为大小写类型,或者在事例类型为可为 null 的值类型时转换为基础值类型。
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
对于模式匹配,编译器会调用 TryGetValue 类型模式检查(例如 union is T)和 HasValue null 模式检查(例如 union is null),避免对值类型事例进行装箱。 每个成员都适用于其自身的模式类型 ,这两者都不是另一个成员的回退。 缺少成员时,编译器会改为检查 Value 该模式的属性。 有关详细信息,请参阅 编译器如何生成代码进行模式匹配。
联合成员提供程序
联合类型可以将其联合成员委托给直接包含和实现的公共嵌套 IUnionMembers 接口。 该接口是 联合成员提供程序。 编译器仅在提供程序接口上查找联合成员,包括继承的成员。 仅对联合类型本身声明的成员不用作联合成员。
使用联合成员提供程序时,接口必须声明以下必需成员:
-
静态
Create方法:一个用于每个事例类型,具有单个参数和一个可转换为联合类型的返回类型标识。 这些方法可建立案例类型。 -
Value属性:具有get返回包含值的访问器的公共object或object?属性。
可以选择在接口上声明这些成员,以便编译器生成更高效的模式匹配代码:
-
TryGetValue方法:一个用于每种事例类型,使用out事例类型的参数返回bool。 -
HasValue属性:一个get公共bool属性,其访问器true返回时间Value不是null。
编译器如何生成用于模式匹配的代码: TryGetValue 并 HasValue 应用于不同类型的模式,这两种模式都不是另一种模式的回退:
- 对于 类型模式,例如
union is T,编译器在声明时调用TryGetValue(out T value)、提取值并应用模式而不进行装箱。 否则,编译器会将模式应用于Value属性,该属性针对T该属性执行运行时类型检查,并且可以在值为值类型时T对值进行装箱。 - 对于 null 模式,例如
union is null,编译器在声明以测试联合是否包含值时调用HasValue。 否则,编译器将 null 模式直接应用于Value属性。
每个成员都是独立可选的:不使用TryGetValue类型模式;Value不使用 HasValuenull 模式。Value 声明任一成员可让编译器为该模式类型生成更高效的强类型代码:
[System.Runtime.CompilerServices.Union]
public struct Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
// Optional but recommended: TryGetValue enables efficient pattern matching
bool TryGetValue(out T value);
bool TryGetValue(out Exception value);
}
object? IUnionMembers.Value => _value;
public bool TryGetValue(out T value)
{
if (_value is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
public bool TryGetValue(out Exception value)
{
if (_value is Exception e)
{
value = e;
return true;
}
value = default!;
return false;
}
}
当联合类型需要专用构造函数或创建逻辑需要工厂模式(例如联合 record class 类型)时,联合成员提供程序非常有用。
基于类的联合类型
类也可以是联合类型。 需要引用语义或继承时,这种类型的联合非常有用:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
对于基于类的联合,模式 null 与 null 引用和 null Value匹配。
C# 语言规范
有关详细信息,请参阅 Unions 功能规范。