通过


联合类型(C# 参考)

联合类型表示可以是多个事例类型之一的值。 联合提供每种事例类型的隐式转换、详尽的模式匹配和增强的可为空性跟踪。 使用 union 关键字声明联合类型:

public union Pet(Cat, Dog, Bird);

此声明创建一个包含三种 Pet 事例类型的联合: CatDogBird。 可以将任何大小写类型值 Pet 分配给变量。 编译器确保 switch 表达式涵盖所有事例类型。

C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。

本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。

小窍门

若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。

当值必须正好是一组固定类型之一时声明联合,并且你希望编译器强制处理每种可能性。 常见方案包括:

  • 结果或错误返回:方法返回成功值或错误值,调用方必须同时处理这两者。 一个联合,如 union Result(Success, Error) 使结果集明确。
  • 消息或命令调度:系统处理一组关闭的消息类型。 联合可确保新消息类型在尚未处理这些消息的每个 switch 类型上生成编译时警告。
  • 替换标记接口或抽象基类:如果只使用接口或抽象类来分组类型进行模式匹配,则联合将为你提供详尽检查,而无需继承或共享成员。

联合不同于其他类型声明的重要方式:

  • 与或classstruct不同的是,联合不会定义新的数据成员。 而是将现有类型组合到一组封闭的替代项中。
  • 与关闭 interface的联合不同,在声明中定义事例类型的完整列表,编译器使用该列表进行详尽检查。
  • 与 a record不同,联合不会添加相等性、克隆或析构行为。 联合侧重于“哪一种情况?”,而不是“它有什么字段?”

重要

在 .NET 11 预览版 2 中,运行时不包含 UnionAttributeIUnion 接口。 若要使用联合类型,必须自行声明它们。 若要查看所需的声明,请参阅 Union 实现

联合声明

联合声明指定事例类型的名称和列表:

public union Pet(Cat, Dog, Bird);

事例类型 可以是转换为 object的任何类型,包括类、结构、接口、类型参数、可为 null 的类型和其他联合。 以下示例显示了不同的事例类型可能性:

public record class Cat(string Name);
public record class Dog(string Name);
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),则值在联合 Value 属性中存储时进行装箱。 联合将其内容存储为单个 object? 引用。

联合声明可以包含包含其他成员的正文,就像结构一样,但存在一些限制。 联合声明不能包括实例字段、自动属性或类似字段的事件。 也不能使用单个参数声明公共构造函数,因为编译器将这些构造函数生成为联合创建成员:

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

联合转换

存在从每个事例类型到联合类型的隐式 联合转换 。 无需显式调用构造函数:

static void BasicConversion()
{
    Pet pet = new Dog("Rex");
    Console.WriteLine(pet.Value); // output: Dog { Name = Rex }

    Pet pet2 = new Cat("Whiskers");
    Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}

联合转换的工作原理是调用相应的生成的构造函数。 如果同一类型存在用户定义的隐式转换运算符,则用户定义的运算符优先于联合转换。 有关转换优先级的详细信息,请参阅 语言规范

联合转换为可以为 null 的联合结构(T?)也适用于 T 联合类型:

static void NullableUnionExample()
{
    Pet? maybePet = new Dog("Buddy");
    Pet? noPet = null;

    Console.WriteLine(Describe(maybePet)); // output: Dog: 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",
    };
}

联合匹配

在联合类型上进行模式匹配时,模式将应用于联合 Value 的属性,而不是联合值本身。 此“解包”行为意味着联合对模式匹配是透明的:

static void PatternMatching()
{
    Pet pet = new Dog("Rex");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

这两种模式是此规则的例外: var 模式和放弃 _ 模式适用于联合值本身,而不是其 Value 属性。 用于在返回 /a0> 时捕获联合值:

if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }

在逻辑模式中,每个分支分别遵循解包规则。 以下模式测试 Pet? 不为 null Value 不为 null:

GetPet() switch
{
    var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks Value
}

注释

由于模式适用于 Value,因此 pet is Pet 一种模式通常不匹配,因为 Pet 会针对联合 的内容 进行测试,而不是联合本身。

Null 匹配

对于结构联合,模式 null 检查是否 Value 为 null:

static void NullHandling()
{
    Pet pet = default;
    Console.WriteLine(pet.Value is null); // output: True

    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
}

对于基于类的联合, 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当表达式处理联合的所有事例类型时,表达式是详尽的。 仅当未处理事例类型时,编译器才会发出警告。 无需包括放弃模式(_)或 var 模式以匹配任何类型:

static void PatternMatching()
{
    Pet pet = new Dog("Rex");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

如果联合属性的 Value null 状态为“可能为 null”,则还必须处理 null 以避免警告:

static void NullHandling()
{
    Pet pet = default;
    Console.WriteLine(pet.Value is null); // output: True

    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
}

可空性

编译器通过以下规则跟踪 union 属性的 Value null 状态:

  • 从事例类型(通过构造函数或联合转换)创建联合值时, Value 获取传入值的 null 状态。
  • 当非装箱访问模式HasValueTryGetValue(...)成员查询联合的内容时,分支上的 true null 状态Value将变为“非 null”。

自定义联合类型

编译器将 union 声明转换为 struct 声明。 结构用 [System.Runtime.CompilerServices.Union] 特性标记,实现 IUnion 接口。 它包括公共构造函数和每个事例类型的隐式转换以及一个 Value 属性。 生成的形式是有意见的。 它始终是一个结构,始终装箱值类型的事例,并且始终将内容存储为 object?

如果需要不同的行为(例如基于类的联合、自定义存储策略、互操作支持或想要调整现有类型),可以手动创建联合类型。

如果属性遵循基本联合模式,则任何具有[Union]属性的类或结构都是联合类型。 基本联合模式需要:

  • [Union] 类型的属性。
  • 一个或多个公共构造函数,每个构造函数都有一个按值或 in 参数。 每个构造函数的参数类型定义 大小写类型
  • 具有访问器类型的object?公共Value属性(或object)。get

所有工会成员都必须公开。 编译器使用这些成员来实现联合转换、模式匹配和详尽性检查。 还可以实现 非装箱访问模式 或创建 基于类的联合类型

编译器假定自定义联合类型满足以下行为规则:

  • 声音Value 始终返回 null 或事例类型之一的值 - 从不返回其他类型的值。 对于结构联合,default生成一nullValue
  • 稳定性:如果从事例类型创建联合值, 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返回值并通过参数传递值的out每种事例类型的bool方法。
[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 属性 Value ,从而避免装箱值类型。

基于类的联合类型

类也可以是联合类型。 需要引用语义或继承时,这种类型的联合非常有用:

[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匹配。

联合实现

以下属性和接口在编译时和运行时支持联合类型:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
    public sealed class UnionAttribute : Attribute;

    public interface IUnion
    {
        object? Value { get; }
    }
}

编译器实现 IUnion的联合声明。 可以使用以下方法 IUnion在运行时检查任何联合值:

if (value is IUnion { Value: null }) { /* the union's value is null */ }

声明类型 union 时,编译器将生成实现 IUnion的结构。 例如, Pet 声明 (public union Pet(Cat, Dog, Bird);) 等效于:

[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; }
}

重要

在 .NET 11 预览版 2 中,这些类型不包括在运行时中。 若要使用联合类型,必须在项目中声明它们。 它们将包含在未来的 .NET 预览版中。

C# 语言规范

有关详细信息,请参阅 Unions 功能规范。

另请参阅