联合类型(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不同,联合不会添加相等性、克隆或析构行为。 联合侧重于“哪一种情况?”,而不是“它有什么字段?”

联合声明

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

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? 引用。

联合声明可以包含包含其他成员的正文,就像结构一样,但存在一些限制。 联合声明不能包括实例字段、自动属性或类似字段的事件。 也不能使用单个参数声明公共构造函数,因为编译器将这些构造函数生成为联合创建成员。 以下 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");
    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 模式和 not 模式应用于联合值本身,而不是其 Value 属性。 用于在返回 /a0> 时捕获联合值:

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

在逻辑模式中,每个分支分别遵循解包规则。 模式的 and 左分支可以更改右分支看到的传入值。 由于模式 not 适用于传入的联合值而不是它 Value,因此前导 not null 不会解包其后面的分支的值:

GetPet() switch
{
    // 'var pet' captures the Pet?; 'not null' applies to the Pet? value (not pet.Value)
    var pet and not null => ...,
    // 'not null' doesn't unwrap to Pet, so 'var value' still captures the Pet?
    not null and var 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 ,可能会出现这种情况,如前面的示例所示。

可空性

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

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

自定义联合类型

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

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

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

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

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

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

  • 声音Value 始终返回 null 或事例类型之一的值 - 从不返回其他类型的值。 对于结构联合,default生成一Valuenull
  • 稳定性:如果从事例类型创建联合值, 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方法。 TryGetValue true仅当该事例类型的非 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 属性 Value ,从而避免装箱值类型。

联合成员提供程序

联合类型可以将其联合成员委托给嵌套 IUnionMembers 接口。 如果存在此接口,编译器将查找 Create 工厂方法,而不是构造函数:

[System.Runtime.CompilerServices.Union]
public record class 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; }
    }

    object? IUnionMembers.Value => _value;
}

当联合类型需要专用构造函数或创建逻辑需要工厂模式(例如联合 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匹配。

联合实现

联合类型依赖于UnionAttribute命名空间中的System.Runtime.CompilerServices类型和IUnion类型。 运行时包括以下类型,从 .NET 11 预览版 5 开始:

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

C# 语言规范

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

另请参阅