Типы союзов (справочник по C#)

Тип объединения представляет значение, которое может быть одним из нескольких типов вариантов. Объединения обеспечивают неявные преобразования из каждого типа регистра, исчерпывающего сопоставления шаблонов и расширенного отслеживания допустимости null. Используйте ключевое union слово для объявления типа объединения:

public union Pet(Cat, Dog, Bird);

Это объявление создает Pet объединение с тремя типами регистров: Cat, Dogи Bird. Для переменной можно назначить любое значение типа регистра Pet . Компилятор гарантирует, что switch выражения охватывают все типы вариантов.

Справочные документы по языку C# описывают последнюю выпущенную версию языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей об истории версий языка C#.

Объявите объединение, когда значение должно быть именно одним из фиксированных типов, и компилятор должен принудительно применить все возможности. Ниже приведены распространенные сценарии.

  • Возвращает результат или ошибку: метод возвращает значение успешного выполнения или значение ошибки, а вызывающий объект должен обрабатывать оба. Объединение, например union Result(Success, Error) , делает набор результатов явным.
  • Отправка сообщений или команд: система обрабатывает закрытый набор типов сообщений. Объединение гарантирует, что новые типы сообщений создают предупреждения во время компиляции на всех switch устройствах, которые еще не обрабатывают их.
  • Замена интерфейсов маркеров или абстрактных базовых классов: если вы используете интерфейс или абстрактный класс исключительно для группирования типов шаблонов для сопоставления шаблонов, объединение обеспечивает исчерпывающую проверку без необходимости наследования или общих членов.

Объединение отличается от других объявлений типов важными способами:

  • class В отличие от элемента или structобъединения, не определяются новые члены данных. Вместо этого он создает существующие типы в закрытый набор альтернативных вариантов.
  • interfaceВ отличие от объединения, вы определяете полный список типов вариантов в объявлении, и компилятор использует этот список для проверки исчерпывающей готовности.
  • 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 свойству. Используется var для записи значения объединения при GetPet() возврате Pet? значения (Nullable<Pet>):

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
}

Если значение NULL свойства объединения Value равно "может быть 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 выражение является значением по умолчанию или не определенно назначено, как показано в предыдущем примере.

Нуллабельность

Компилятор отслеживает состояние NULL свойства объединения Value с помощью следующих правил:

  • Значение null по умолчанию для свойства объединения Value — "может быть null", если состояние null по умолчанию любого типа регистра равно "может быть null". В противном случае значение null по умолчанию равно null.
  • При создании значения объединения из типа регистра (с помощью конструктора или преобразования объединения) Value получает значение NULL для входящего значения.
  • Когда шаблон HasValue доступа, отличный от поля, запрашивает TryGetValue(...) содержимое объединения, состояние Value NULL становится "не null" в true ветви.

Пользовательские типы объединения

Компилятор преобразует 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 Свойство типаbool, которое возвращаетсяtrue, когда Value это не nullтак.
  • TryGetValue Метод для каждого типа регистра, который возвращает bool и передает значение через out параметр. TryGetValue возвращается true только в том случае, если Value значение этого типа регистра не равно null. 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
}

Компилятор предпочитает TryGetValueValue свойство при реализации сопоставления шаблонов, что позволяет избежать типов значений бокса.

Поставщики членов профсоюза

Тип объединения может делегировать его члены объединения в вложенный 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 типов и 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#

Дополнительные сведения см. в спецификации компонентов Профсоюзов .

См. также