联合类型(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 类型模式检查(例如 union is T)和 HasValue null 模式检查(例如 union is null),避免对值类型事例进行装箱。 每个成员都适用于其自身的模式类型 ,这两者都不是另一个成员的回退。 缺少成员时,编译器会改为检查 Value 该模式的属性。 有关详细信息,请参阅 编译器如何生成代码进行模式匹配

联合成员提供程序

联合类型可以将其联合成员委托给嵌套 IUnionMembers 接口。 当此接口存在时,该 union 类型的行为为 联合成员提供程序,编译器仅生成对声明 IUnionMembers的成员的调用。 编译器不使用在联合类型本身上声明的成员,但从接口中省略 IUnionMembers

使用联合成员提供程序时,接口必须声明以下必需成员:

  • 静态 Create 方法:一个用于每个事例类型,具有单个参数和一个可转换为联合类型的返回类型标识。 这些方法可建立案例类型。
  • Value属性:具有get返回包含值的访问器的公共objectobject?属性。

可以选择在接口上声明这些成员,以便编译器生成更高效的模式匹配代码:

  • TryGetValue方法:一个用于每种事例类型,使用out事例类型的参数返回bool
  • HasValue属性:一个get公共bool属性,其访问器true返回时间Value不是 null

编译器如何生成用于模式匹配的代码TryGetValueHasValue 应用于不同类型的模式,这两种模式都不是另一种模式的回退:

  • 对于 类型模式,例如 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匹配。

联合实现

联合类型依赖于UnionAttribute命名空间中的IUnion类型和System.Runtime.CompilerServices类型。 运行时包括以下类型,从 .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 功能规范。

另请参阅