Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Тип объединения представляет значение, которое может быть одним из нескольких типов вариантов. Объединения обеспечивают неявные преобразования из каждого типа регистра, исчерпывающего сопоставления шаблонов и расширенного отслеживания допустимости 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В отличие от объединения, не добавляется равенство, клонирование или деконструкция поведения. Профсоюз фокусируется на "какой случай это?" а не "какие поля у него есть?"
Типы объединения зависят от 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. |
Например, определите объединение со свойством Pet только для Name чтения, которое делегирует содержавшемся Cat или Dog. Шаблоны типов в выражении свойства 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" } считывает свойство объединения Name , поэтому он также соответствует коту с именем Rex:
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 это полезный тест на наличие значения. В отличие от этого, пустой шаблон { }нетипизированного свойства проверяет экземпляр объединения. Он всегда соответствует ненулевому союзу структуры, даже если 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.
var В not null and var petшаблоне по-прежнему фиксируется входящий союз: not не изменяет значение, переданное в шаблон справа. В отличие от этого, Dog and { Breed: "Labrador" }шаблон типа передает соответствующий Dog шаблон свойству.
Замечание
Шаблон типа должен быть совместим по крайней мере с одним типом регистра.
Pet Для объявлений в этой статье pet is Pet создается ошибка компилятора CS8121: ни один из их типов регистров не может соответствоватьPet. Это не тест среды выполнения экземпляра объединения. Чтобы проверить содержимое, использовать pet is not null или pet.Value is not null; для записи самого объединения, используйте var шаблон.
Короче говоря, шаблоны типов и объявлений проверяют содержимое. Рекурсивные шаблоны с типом также проверяют содержимое; рекурсивные шаблоны без типа используют собственные члены профсоюза. Эти правила одинаковы в is выражении и switch выражении.
Сопоставление значений 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 рука:
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 не указать руку. Например, 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, включая, когда это инициализированное по умолчанию поле или элемент массива. Если все типы случаев являются ненуклюжими, отслеживаемое по умолчанию состояние 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 руки не возникает никаких предупреждений без ошибки-null-arm, если только предыдущий код не изменяет отслеживаемое состояние NULL, но при получении значения по умолчанию параметр вызывает исключение SwitchExpressionException . Более ранний is null тест может изменить отслеживаемое состояние и вызвать предупреждение. Это поведение отличается от определенного назначения: компилятор отклоняет чтение неназначенной локальной переменной.
Нуллабельность
Компилятор отслеживает состояние NULL свойства объединения Value с помощью следующих правил:
- Значение null по умолчанию для свойства объединения
Value— "может быть null", если состояние null по умолчанию любого типа регистра равно "может быть null". В противном случае значение null по умолчанию равно null. - При создании значения объединения из типа регистра (с помощью конструктора или преобразования объединения)
Valueполучает значение NULL для входящего значения. - Когда шаблон
HasValueдоступа, отличный от поля, запрашиваетTryGetValue(...)содержимое объединения, состояниеValueNULL становится "не null" вtrueветви.
Пользовательские типы объединения
Компилятор преобразует union объявление в struct объявление. Структура помечена атрибутом [System.Runtime.CompilerServices.Union] и реализует IUnion интерфейс. Он включает открытый конструктор для каждого типа регистра и Value свойства. Язык предоставляет неявные преобразования объединения, которые вызывают эти конструкторы. Эта сформированная форма считается мнением. Это всегда структуру, всегда поля типов значений и всегда сохраняет содержимое как object?.
Возможно, вам потребуется другое поведение, если вы хотите адаптировать существующий тип, создать объединение на основе классов или использовать настраиваемую стратегию хранения или если вам нужна поддержка взаимодействия. Вы можете создать тип объединения вручную.
Любой класс или структура с [Union] атрибутом является типом объединения , если он соответствует базовому шаблону объединения. Для базового шаблона объединения требуется:
- Атрибут
[Union]типа. - Один или несколько открытых конструкторов, каждый из которых имеет одно значение или
inпараметр. Тип параметра каждого конструктора определяет тип регистра. - Общедоступное
Valueсвойство типаobject?(илиobject) с методомgetдоступа.
Все предыдущие члены профсоюза должны быть общедоступными. Компилятор использует эти члены для реализации преобразований объединения, сопоставления шаблонов и исчерпывающих проверок. Вы также можете реализовать шаблон доступа, отличный от бокса , или создать тип объединения на основе классов. Пользовательский тип объединения может добавлять дополнительные члены.
Компилятор предполагает, что типы пользовательских союзов удовлетворяют этим правилам поведения:
-
Звук:
Valueвсегда возвращает или значение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Свойство типа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
}
Для сопоставления шаблонов компилятор вызывает TryGetValue проверки типов (например union is T), а HasValue также проверки шаблонов null (например union is null), избегая бокса для вариантов типа значений. Каждый элемент применяется к своему собственному типу шаблона— ни один из них не является резервным вариантом для другого. Если элемент отсутствует, компилятор проверяет Value свойство для этого шаблона. Дополнительные сведения см. в статье о том, как компилятор создает код для сопоставления шаблонов.
Поставщики членов профсоюза
Тип объединения может делегировать его члены объединения в общедоступный вложенный IUnionMembers интерфейс, который он напрямую содержит и реализует. Этот интерфейс является поставщиком членов профсоюза. Компилятор находит члены объединения только в интерфейсе поставщика, включая унаследованные элементы. Члены, объявленные только для самого типа профсоюза, не используются в качестве членов профсоюза.
При использовании поставщика членов профсоюза интерфейс должен объявить следующие обязательные члены:
-
Статические
Createметоды: один для каждого типа регистра с одним параметром и типом возвращаемого типа, преобразованным в тип объединения. Эти методы устанавливают типы вариантов. -
Valueсвойство: общедоступноеobjectилиobject?свойство с методомgetдоступа, который возвращает содержащееся значение.
При необходимости можно объявить эти элементы в интерфейсе, чтобы компилятор создает более эффективный код сопоставления шаблонов:
-
TryGetValueметоды: один для каждого типа регистра, возвращающийсяboolс параметромoutтипа регистра. -
HasValueсвойство: общедоступноеboolсвойство с методомgetдоступа, возвращающимсяtrue, еслиValueэто неnullтак.
Как компилятор создает код для сопоставления шаблонов: и HasValue применяется к различным типам шаблонов— TryGetValue ни для другого:
- Для шаблона типа, например
union is T, компилятор вызываетсяTryGetValue(out T value)при объявлении, извлекая значение и применяя к нему шаблон без бокса. В противном случае компилятор применяет шаблон кValueсвойству, который выполняет проверкуTтипа среды выполнения и может учесть значение, еслиTэто тип значения. - Для шаблона NULL, например
union is null, компилятор вызываетсяHasValueпри объявлении для проверки того, содержит ли объединение значение. В противном случае компилятор применяет шаблон NULL к свойствуValueнапрямую.
Каждый член является независимым необязательным: без TryGetValueшаблонов типов используется Value; без HasValueиспользования шаблонов ValueNULL. Объявление любого члена позволяет компилятору создавать более эффективный, строго типизированный код для этого типа шаблона:
[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#
Дополнительные сведения см. в спецификации компонентов Профсоюзов .