Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отражены в соответствующих заметках с заседания по дизайну языка (LDM) .
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/9662
Сводка
Профсоюзы — это набор взаимосвязанных функций, которые объединяются для предоставления поддержки C# для типов профсоюзов:
-
Типы профсоюзов: структуры и классы, имеющие
[Union]атрибут, распознаются как типы объединения и поддерживают поведение объединения. - Типы вариантов: типы объединения имеют набор типов вариантов, которые задаются параметрами конструкторов и методов фабрики.
-
Поведение профсоюза: типы союзов поддерживают следующее поведение объединения:
- Преобразования объединения: неявные преобразования объединения из каждого типа регистра в тип объединения.
- Сопоставление объединения: сопоставление шаблонов со значениями объединения неявно "распаковывает" их содержимое, применяя шаблон к базовому значению.
- Исчерпывающая связь. Переключение выражений по значениям объединения является исчерпывающим, если все типы случаев были сопоставлены без необходимости в резервном случае.
- Допустимость объединения. Анализ допустимости null позволяет отслеживать состояние null содержимого объединения.
- Шаблоны объединения: все типы профсоюзов соответствуют базовому шаблону объединения, но существуют дополнительные дополнительные шаблоны для конкретных сценариев.
- Объявления союза: короткий синтаксис позволяет напрямую объявлять типы профсоюзов. Реализация является "мнением" — объявление структуры, которое следует базовому шаблону объединения и сохраняет содержимое в виде одного ссылочного поля.
- Интерфейсы объединения: несколько интерфейсов известны языком и используются в реализации объявлений профсоюзов.
Мотивация
Объединения — это функция C# с длинным запросом, которая позволяет выразить значения из закрытого набора типов таким образом, чтобы сопоставление шаблонов может быть исчерпывающим.
Разделение между типамипрофсоюзов и объявлениями профсоюзов позволяет C# иметь краткий синтаксис объявления объединения с семантикой, а также разрешать существующие типы или типы с другими вариантами реализации для выбора поведения объединения.
Предлагаемые профсоюзы в C# являются объединениями типов , а не "дискриминированными" или "помеченными". "Дискриминированные профсоюзы" можно выразить с точки зрения "профсоюзов типов" с помощью новых объявлений типов в качестве типов. Кроме того, они могут быть реализованы как закрытая иерархия, которая является другой, связанной с предстоящей функцией C#, ориентированной на исчерпывающую.
Подробный дизайн
Типы объединения
Любой класс или тип структуры с атрибутом System.Runtime.CompilerServices.UnionAttribute считается типом объединения:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(Class | Struct, AllowMultiple = false)]
public class UnionAttribute : Attribute;
}
Тип профсоюза должен соответствовать определенной схеме членов общественного союза, которые должны быть объявлены в самом типе профсоюза или делегированы "поставщику членов профсоюза".
Некоторые члены профсоюза являются обязательными, и другие являются необязательными.
Тип объединения имеет набор типов регистров , которые устанавливаются на основе подписей определенных членов профсоюза.
Доступ к содержимому значения объединения можно получить через Value свойство. Язык предполагает, что Value только когда-либо содержит значение одного из типов регистра или null (см. хорошо сформированность).
Поставщики членов профсоюза
По умолчанию члены профсоюза находятся в самом типе объединения. Однако если тип объединения напрямую содержит объявление интерфейса, вызываемого IUnionMembers , интерфейс выступает в качестве поставщика членов профсоюза. В этом случае члены профсоюза находятся только в поставщике членов профсоюза, а не в самом типе профсоюза.
Интерфейс поставщика членов профсоюза должен быть общедоступным, и сам тип объединения должен реализовать его в качестве интерфейса.
Мы используем тип, определяющий объединение , для типа, в котором найдены члены профсоюза: поставщик членов профсоюза, если он существует, и сам тип объединения в противном случае.
Члены профсоюза
Члены профсоюза ищутся по имени и подписи в типе, определяемом объединением. Они не должны быть объявлены непосредственно в типе, определяемом объединением, но могут быть унаследованы.
Это ошибка для любого члена профсоюза, не являющегося публичной.
Элементы создания и Value свойство являются обязательными и совместно называются основным шаблоном объединения.
TryGetValue Члены HasValue совместно называются шаблоном доступа к профсоюзу без бокса.
Различные члены профсоюза описаны ниже.
Члены создания союза
Элементы создания объединения используются для создания новых значений объединения из значения типа регистра.
Если тип, определяющий объединение, сам является типом объединения, каждый конструктор с одним параметром является конструктором объединения. Типы регистров объединения определяются как набор типов, созданных из типов параметров этих конструкторов следующим образом:
- Если тип параметра является типом, допускаемым значением null (независимо от значения или ссылки), тип регистра является базовым типом.
- В противном случае тип регистра — это тип параметра.
// Union constructor making `Dog` a case type
public Pet(Dog value) { ... }
// Union constructor making `int` a case type
public Union(int? value) { ... }
// Union constructor making `string` a case type
public Union(string? value) { ... }
Если тип объединения является поставщиком членов объединения, каждый статический Create метод с одним параметром и возвращаемым типом, который преобразуется в тип объединения, является методом фабрики объединения.
Типы регистров объединения определяются как набор типов, созданных из типов параметров этих методов фабрики следующим образом:
- Если тип параметра является типом, допускаемым значением null (независимо от значения или ссылки), тип регистра является базовым типом.
- В противном случае тип регистра — это тип параметра.
// Union factory method making `Cat` a case type
public static Pet Create(Cat value) { ... }
// Union factory method making `int` a case type
public static Union Create(int? value) { ... }
// Union factory method making `string` a case type
public static Union Create(string? value) { ... }
Конструкторы профсоюзов и методы фабрики профсоюзов называются коллективно членами создания профсоюза.
Один параметр элемента создания объединения должен быть по значению или in параметру.
Тип объединения должен иметь по крайней мере один член создания профсоюза и, следовательно, по крайней мере один тип дела.
Свойство Value
Свойство Value разрешает доступ к значению, содержаемого в союзе, независимо от типа регистра.
Каждый определяющий объединение тип должен объявлять Value свойство типа object? или object. Свойство должно иметь get метод доступа и при необходимости может иметь init или set метод доступа, который может быть любой специальных возможностей и не используется компилятором.
// Union 'Value' property
public object? Value { get; }
Элементы доступа, отличные от бокса
Тип объединения может дополнительно реализовать шаблон доступа к объединениям, который позволяет строго типизированному условному доступу к каждому типу регистра, а также способ проверить значение NULL.
Это позволяет компилятору реализовать сопоставление шаблонов более эффективно, если типы регистров являются типами значений и хранятся как таковые в союзе.
Члены доступа, отличные от коробки, являются следующими:
-
HasValueСвойство типаboolс общедоступнымgetметодом доступа. При необходимости он может иметьinitилиsetметод доступа, который может быть любой специальных возможностей и не используется компилятором. -
TryGetValueМетод для каждого типа регистра. Метод возвращаетboolи принимает отдельный параметр типа, который преобразуется в тип регистра.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }
HasValue Ожидается, что значение true возвращается, если и только в том случае, если профсоюз Value не имеет значения NULL.
TryGetValue Ожидается, что возвращает значение true, если и только если объединение Value имеет заданный тип регистра, и если да, доставляет это значение в параметре out метода.
Хорошо сформированность
Язык и компилятор делают ряд предположений о типах объединения. Если тип квалифифизируется как тип объединения, но не удовлетворяет этим предположениям, поведение объединения может не работать должным образом.
-
Звук:
Valueсвойство всегда оценивается как null, так и значение типа регистра. Это верно даже для значения по умолчанию типа объединения. -
Стабильность: если значение объединения создается из типа регистра,
Valueсвойство будет соответствовать данному типу регистра или null. Если значение объединения создается изnullзначения,Valueсвойство будетnull. - Эквивалентность создания. Если значение неявно преобразуется в два разных типа регистра, то элемент создания для любого из этих типов случаев имеет то же наблюдаемое поведение при вызове с этим значением.
-
Согласованность шаблонов доступа: поведение элементов доступа,
TryGetValueотличныхHasValueот бокса, при наличии, заметно эквивалентно проверке свойстваValueнапрямую.
Примеры типов объединения
Pet реализует базовый шаблон объединения для самого типа объединения:
[Union] public record struct Pet
{
// Creation members = case types are 'Dog' and 'Cat'
public Pet(Dog value) => Value = value;
public Pet(Cat value) => Value = value;
// 'Value' property
public object? Value { get; }
}
IntOrBool реализует шаблон доступа, отличный от бокса, на самом типе объединения:
public record struct IntOrBool
{
private bool _isBool;
private int _value;
public IntOrBool(int value) => (_isBool, _value) = (false, value);
public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);
public object Value => _isBool ? _value is 1 : _value;
public bool HasValue => true;
public bool TryGetValue(out int value)
{
value = _value;
return !_isBool;
}
public bool TryGetValue(out bool value)
{
value = _isBool && _value is 1;
return _isBool;
}
}
Примечание: Это просто пример реализации шаблона доступа, отличного от бокса. Пользовательский код может хранить содержимое каким-либо образом. В частности, это не препятствует реализации бокса! В non-boxing его имени обозначается разрешение реализации сопоставления шаблонов компилятора для доступа к каждому типу регистра строго типизированным способом, а не к свойству object?с типизированным типом Value .
Result<T> реализует базовый шаблон через поставщика членов профсоюза:
public record class Result<T> : Result<T>.IUnionMembers
{
object? _value;
public interface IUnionMembers
{
public static Result<T> Create(T value) => new() { _value = value };
public static Result<T> Create(Exception value) => new() { _value = value };
public object? Value { get; }
}
object? IUnionMembers.Value => _value;
}
Поведение объединения
Поведение объединения обычно реализуется с помощью базового шаблона объединения. Если союз предлагает шаблон доступа без бокса, сопоставление шаблонов объединения будет предпочтительно использовать его.
Преобразования объединения
Преобразование объединения неявно преобразуется в тип объединения из каждого из его типов вариантов. В частности, существует преобразование объединения в тип U объединения из типа или выражения E , если существует стандартное неявное преобразование из E типа C и C является типом параметра членаUсоздания объединения.
Если тип U объединения является структурой, существует преобразование объединения в тип U? или выражение E , если существует стандартное неявное преобразование из E типа в тип C и C является типом параметра членаUсоздания объединения.
Преобразование объединения не является стандартным неявным преобразованием. Поэтому он не может участвовать в неявном преобразовании, определяемом пользователем, или другом преобразовании объединения.
Не существует явных преобразований объединения за пределами неявных преобразований объединения. Таким образом, даже если существует явное преобразование из E типа Cдела профсоюза, то это не означает, что существует явное преобразование из E этого типа объединения.
Преобразование профсоюза выполняется путем вызова члена создания профсоюза:
Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");
Это ошибка, если разрешение перегрузки не находит одного лучшего кандидата или если этот член не является одним из членов профсоюза типа профсоюза.
Преобразование объединения — это просто другая "форма" неявного пользовательского преобразования. Применимое пользовательское преобразование оператора преобразования "тени" объединения.
Обоснование этого решения:
Если пользователь написал определяемый пользователем оператор, он должен получить приоритет. Другими словами, если пользователь на самом деле написал свой собственный оператор, они хотят, чтобы мы его назвали. Существующие типы с операторами преобразования, преобразованными в типы объединения, продолжают работать так же, как и существующий код, используюющий операторы сегодня.
В следующем примере неявное пользовательское преобразование имеет приоритет над преобразованием объединения.
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => ...
public S1(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static implicit operator S1(int x) => ...
}
class Program
{
static S1 Test1() => 10; // implicit operator S1(int x) is used
static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
}
В следующем примере, когда явное приведение используется в коде, явно определяемое пользователем преобразование имеет приоритет над преобразованием объединения. Но при отсутствии явного приведения в код используется преобразование объединения, так как явно определенное пользователем преобразование неприменимо.
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => ...
public S2(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static explicit operator S2(int x) => ...
}
class Program
{
static S2 Test3() => 10; // Union conversion S2.S2(int) is used
static S2 Test4() => (S2)20; // explicit operator S2(int x)
}
Сопоставление объединения
Если входящее значение шаблона имеет тип объединения или значение NULL типа объединения, допустимое значение NULL и содержимое базового значения объединения, может быть "распаковано" в зависимости от шаблона.
Для безусловных _ и var шаблонов шаблон применяется к самому входящему значению. Рассмотрим пример.
if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`
Однако все остальные шаблоны неявно применяются к свойству базового объединения Value :
if (GetPet() is Dog dog) { ... } // 'Dog dog' is applied to 'GetPet().Value'
if (GetPet() is null) { ... } // 'null' is applied to 'GetPet().Value'
if (GetPet() is { } value) { ... } // '{ } value' is applied to 'GetPet().Value'
Для логических шаблонов это правило применяется отдельно к ветвям, учитывая, что левая ветвь шаблона может повлиять на входящий тип правой and ветви:
GetPet() switch
{
var pet and not null => ... // 'var pet' applies to the incoming 'Pet' and 'not null' to its 'Value'
not null and var value => ... // 'not null' applies to the 'Value' as does 'var value' because of the
// left branch changing the incoming type to `object?`.
}
Примечание: Это правило означает, что, скорее всего, GetPet() is Pet pet не будет выполнено успешно, как Pet применяется к содержимому, а не к самому союзу Pet .
Примечание: Причина разного обращения с безусловным var шаблоном (а также _, что, по сути, является сокращением) var _является предположением, что их использование качественно отличается от других шаблонов.
var шаблоны используются просто для имени значения, соответствующего значению, часто в вложенных шаблонах, таких как PetOwner{ Pet: var pet }. Здесь полезная семантика заключается pet в сохранении типа Petобъединения, а не Value для свойства, разыменовываемого на бесполезный object? тип.
Если входящее значение является типом класса, null шаблон будет успешно выполнен независимо от того, является null ли само значение объединения или его содержащимся значением null:
if (result is null) { ... } // if (result == null || result.Value == null)
Другие шаблоны сопоставления объединения будут успешными только в том случае, если само значение объединения не nullявляется.
if (result is 1) { ... } // if (result != null && result.Value is 1)
Аналогичным образом, если входящее значение является типом значений, допускающим значение NULL (обтекание типом объединения структуры), null шаблон будет успешно выполнен независимо от того, является null ли входящее значение само или его содержащееся значение:null
if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)
Другие шаблоны сопоставления объединения будут успешными только в том случае, если входящее значение не nullявляется.
if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)
Компилятор предпочтет реализовать поведение шаблона с помощью элементов, предписанных шаблоном доступа без бокса. Хотя это бесплатно для любой оптимизации в пределах правил хорошо сформированности, следующие минимальные наборы гарантированно применяются:
- Для шаблона, который подразумевает проверку определенного типа
T, еслиTryGetValue(S value)метод доступен, и имеется удостоверение или неявное преобразование ссылок или бокса изTS, то этот метод используется для получения значения. Затем шаблон применяется к данному значению. Если имеется несколько таких методов, то при наличии преобразование изTSне является преобразованием бокса. Если по-прежнему существует несколько методов, один выбирается в определенной реализации способом. - В противном случае для шаблона, который подразумевает проверку наличия
nullсвойства,HasValueэто свойство используется для проверки значения объединения null. - В противном случае шаблон применяется к результату доступа к
IUnion.Valueсвойству входящего объединения.
Оператор is-type , применяемый к типу объединения, имеет то же значение, что и шаблон типа, применяемый к типу объединения.
Исчерпывающее объединение
Предполагается, что тип объединения "исчерпан" его типами вариантов. Это означает, что switch выражение является исчерпывающим, если он обрабатывает все типы вариантов объединения:
var name = pet switch
{
Dog dog => ...,
Cat cat => ...,
// No warning about non-exhaustive switch
};
Нуллабельность
Состояние NULL свойства объединения Value отслеживается как любое другое свойство с этими изменениями:
- При вызове члена создания объединения (явным образом или путем преобразования объединения) новое объединение
Valueполучает значение NULL для входящего значения. - Если шаблон
HasValueдоступа без поля илиTryGetValue(...)используется для запроса содержимого типа объединения (явно или с помощью сопоставления шаблонов), это влияет наValueсостояние null таким же образом, как если быValueбыл установлен прямой флажок: значение NULLValueстановится "не null" вtrueветви.
Даже если переключатель объединения в противном случае является исчерпывающим, если состояние NULL свойства входящего объединения Value равно "может быть null", предупреждение будет дано по необработанным значениям NULL.
Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
Dog dog => ...,
Cat cat => ...,
// Warning: 'null' not handled
}
Интерфейсы объединения
Следующие интерфейсы используются языком в реализации функций объединения.
Интерфейс доступа к союзу
Интерфейс IUnion помечает тип как тип объединения во время компиляции и предоставляет способ доступа к содержимому объединения во время выполнения.
public interface IUnion
{
// The value of the union or null
object? Value { get; }
}
Объединения, созданные компилятором, реализуют этот интерфейс.
Пример использования:
if (value is IUnion { Value: null }) { ... }
Объявления профсоюза
Объявления профсоюзов — это краткий и сужденный способ объявления типов профсоюзов в C#. Они объявляют структуру, которая использует одну ссылку на объект для хранения его Value. Это означает:
- Бокс: любые типы значений между их типами регистров будут включено в запись.
- Сжатие: значения объединения содержат только одно поле.
Цель состоит в том, чтобы объявления профсоюзов охватывали подавляющее большинство вариантов использования довольно хорошо. Предполагается, что две основные причины написания кода для конкретных типов профсоюзов, а не использование объявлений профсоюзов:
- Адаптация существующих типов к шаблонам объединения для получения поведения объединения.
- Реализация другой стратегии хранения, например эффективности или взаимодействия.
Синтаксис
Объявление профсоюза имеет имя и список типов конструкторов профсоюзов .
union_declaration
: attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
'(' type (',' type)* ')' struct_interfaces? type_parameter_constraints_clause*
(`{` struct_member_declaration* `}` | ';')
;
Помимо ограничений на членов структуры (§16.3), следующие действия применяются к членам профсоюза:
- Поля экземпляров, автоматические свойства или события, подобные полям, не допускаются.
- Явным образом объявленные общедоступные конструкторы с одним параметром не допускаются.
- Явно объявленные конструкторы должны использовать
this(...)инициализатор для делегата (прямо или косвенно) одному из созданных конструкторов.
Типы конструкторов объединения могут быть любым типом, который преобразуется в objectинтерфейсы, параметры типа, типы, типы, допускающие значение NULL, и другие объединения. Это хорошо для результирующих случаев перекрытия, а для профсоюзов вложено или иметь значение NULL.
Примеры:
// Union of existing types
public union Pet(Cat, Dog, Bird);
// Union with function member
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
IEnumerable<T> list => list,
T value => [value],
}
}
// "Discriminated" union with freshly declared case types
public record class None();
public record class Some<T>(T value);
public union Option<T>(None, Some<T>);
#### Lowering
A union declaration is lowered to a struct declaration with
* the same attributes, modifiers, name, type parameters and constraints,
* implicit implementations of `IUnion`,
* a `public object? Value { get; }` auto-property,
* a public constructor for each *union constructor* type,
* any members in the union declaration's body.
It is an error for user-declared members to conflict with generated members.
Example:
``` c#
public union Pet(Cat, Dog){ ... }
Ниже приведено значение:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public object? Value { get; }
... // original body
}
Открытые вопросы
[Разрешено] Является ли объявление профсоюза записью?
Объявление профсоюза понижено до структуры записи
Я думаю, что это поведение по умолчанию является ненужным и, учитывая, что он не настраивается, будет значительно ограничивать сценарии использования. Записи создают много кода, который либо не используется, либо не соответствует конкретным требованиям. Например, записи довольно запрещены в базе кода компилятора из-за этого большого объема кода. Я думаю, что лучше изменить значение по умолчанию:
- По умолчанию объявление профсоюза объявляет обычную структуру с только членами профсоюза.
- Пользователь может объявить объединение записей:
record union U(E1, ...) ...
Разрешение: Объявление профсоюза является простой структурой, а не структурой записи. Не record union ... поддерживается
[Разрешено] Синтаксис объявления объединения
Это выглядит так, как предлагаемый синтаксис является неполным или ненужным ограничением. Например, это выглядит так, как базовое предложение не допускается. Тем не менее, я могу легко представить, что необходимо реализовать интерфейс, например.
Я думаю, что помимо списка типов элементов синтаксис должен соответствовать регулярному struct/record struct объявлению, где struct ключевое слово заменяется ключевым словом.union
Разрешение: Ограничение удаляется.
[Разрешено] Члены декларации союза
Поля экземпляров, автоматические свойства или события, подобные полям, не допускаются.
Это чувствует себя произвольным и абсолютно ненужным.
Разрешение: Ограничение сохраняется.
[Разрешено] Типы значений, допускающие значение NULL, как типы регистров Union
Типы регистров объединения определяются как набор типов параметров из этих конструкторов. Типы регистров объединения определяются как набор типов параметров из этих методов фабрики.
Одновременно:
TryGetValueМетод для каждого типа регистра. Метод возвращаетboolи принимает отдельный параметр типа, соответствующего заданному типу регистра, следующим образом:
- Если тип регистра является типом значения, допускающего значение NULL, тип параметра должен быть преобразован в базовый тип.
- В противном случае тип должен быть преобразован в тип регистра.
Есть ли преимущество иметь тип значения NULL среди типов регистров, особенно что шаблон типа не может использовать тип значения NULL в качестве целевого типа? Похоже, что можно просто сказать, что если тип параметра конструктора или фабрики является типом значения, допускающим значение NULL, то соответствующий тип регистра является базовым типом. Тогда нам не потребуется это дополнительное предложение для TryGetValue метода, все параметры out являются типами вариантов.
Разрешение: Предложение утверждено
[Разрешено] Состояние свойства, допускающего Value значение NULL по умолчанию
Для типов объединения, в которых ни один из типов регистра не имеет значения NULL, состояние по умолчанию для
Valueпараметра "не равно NULL", а не "может быть null".
С новым дизайном, где Value свойство не определено в каком-то общем интерфейсе, но является API, который конкретно принадлежит объявленному типу, правило, приведенное выше, чувствует себя как чрезмерное проектирование. Кроме того, правило, скорее всего, приведет к тому, что потребители будут использовать типы, допускающие значение NULL, в ситуациях, когда в противном случае типы, допускающие значение NULL, не будут использоваться.
Например, рассмотрим следующее объявление профсоюза:
union U1(int, bool, DateTime);
В соответствии с приведенным правилом состояние Value по умолчанию — "не null". Но это не соответствует поведению типа, default(U1).Value является null. Чтобы изменить поведение, потребитель вынужден сделать по крайней мере один тип регистра, допускающий значение NULL. Что-то подобное:
union U1(int?, bool, DateTime);
Но это, вероятно, нежелательно, потребитель может не разрешить явное создание со значением int? .
Предложение. Удалите кавычек правило, анализ, допускающий значение NULL, должен использовать заметки из Value свойства для вывода его допустимости по умолчанию.
Разрешение: Предложение утверждено
[Разрешено] Сопоставление объединения для значения NULL типа значения объединения
Если входящее значение шаблона имеет тип объединения, содержимое значения объединения может быть "распаковано" в зависимости от шаблона.
Следует ли развернуть это правило в сценариях, когда входящее значение шаблона имеет значение Nullable<union type>?
Рассмотрим следующий сценарий.
static bool Test1(StructUnion? u)
{
return u is 1;
}
static bool Test2(ClassUnion? u)
{
return u is 1;
}
Смысл u is 1 в Test1 и Test2 очень отличается. В Test1 это не сопоставление объединения в Test2.
Возможно, "сопоставление объединения" должно "копать" через Nullable<T> как сопоставление шаблонов обычно делает в других ситуациях.
Если мы идем с этим, то шаблон сопоставления nullNullable<union type> объединения должен работать в отношении классов.
Т. е. шаблон является истинным, когда (!nullableValue.HasValue || nullableValue.Value.Value is null).
Разрешение: Это предложение утверждено.
Что делать с "плохими" API?
Что следует делать компилятору о сопоставлениях API объединения, которые выглядят как совпадение, но в противном случае "плохо"? Например, компилятор находит TryGetValue/HasValue с соответствующей сигнатурой, но это "плохо", так как требуется настраиваемый модификатор или требуется неизвестная функция и т. д. Должен ли компилятор автоматически игнорировать API или сообщить об ошибке? Аналогичным образом API может быть помечен как устаревший или экспериментальный. Должен ли компилятор сообщить о какой-либо диагностике, автоматически использовать API или автоматически не использовать API?
Что делать, если типы для объявления профсоюза отсутствуют
Что произойдет, если UnionAttributeили IUnion<TUnion>IUnion отсутствует? Ошибка? Синтезировать? Что-то другое?
[Разрешено] Проектирование универсального интерфейса IUnion
Аргументы были сделаны, которые IUnion<TUnion> не должны наследоваться от IUnion параметра типа или ограничивать его параметр IUnion<TUnion>типа. Мы должны пересмотреть.
Разрешение: Интерфейс IUnion<TUnion> теперь удален.
[Разрешено] Типы значений, допускающие значение NULL, как типы регистров и их взаимодействие с TryGetValue
В приведенном выше правил указано, что если тип регистра является типом значения, допускающим значение NULL, тип параметра, используемый в соответствующем TryGetValue методе, должен быть базовым типом.
Это обусловлено тем, что null значение никогда не будет даваться с помощью этого метода. На стороне потребления тип значения, допускающий значение NULL, не допускается как шаблон типа, в то время как совпадение с базовым типом должно соответствовать вызову этого метода.
Мы должны подтвердить, что мы согласны с этим распаку.
Разрешение: Согласовано или подтверждено
Шаблон доступа к союзу без бокса
Необходимо указать точные правила для поиска подходящих HasValue и TryGetValue API.
Участвует ли наследование? Является ли чтение и запись HasValue приемлемым совпадением? И так далее.
[Разрешено] TryGetValue сопоставления преобразований
В разделе "Сопоставление профсоюзов" говорится:
Для шаблона, который подразумевает проверку конкретного типа
T, еслиTryGetValue(S value)метод доступен, и существует неявное преобразование изTS, то этот метод используется для получения значения.
Ограничен ли набор неявных преобразований каким-либо образом? Например, разрешены ли пользовательские преобразования? Что касается преобразований кортежей и других не так тривиальных преобразований? Некоторые из них являются даже стандартными преобразованиями.
Ограничен ли набор TryGetValue методов каким-либо другим способом? Например, раздел "Шаблоны союза" подразумевает, что рассматриваются только методы с типом параметра, соответствующим типу регистра:
public bool TryGetValue(out T value)метод для каждого типаTрегистра.
Было бы хорошо иметь явный ответ.
Разрешение: Рассматриваются только неявные удостоверения, ссылки или преобразования бокса
TryGetValue и анализ, допускающий значение NULL
Если шаблон
HasValueдоступа без поля илиTryGetValue(...)используется для запроса содержимого типа объединения (явно или с помощью сопоставления шаблонов), это влияет наValueсостояние null таким же образом, как если быValueбыл установлен прямой флажок: значение NULLValueстановится "не null" вtrueветви.
Ограничен ли набор TryGetValue методов каким-либо образом? Например, раздел "Шаблоны союза" подразумевает, что рассматриваются только методы с типом параметра, соответствующим типу регистра:
public bool TryGetValue(out T value)метод для каждого типаTрегистра.
Было бы хорошо иметь явный ответ.
Уточняйте правила относительно default значений типов объединения структур
Примечание. Указанное ниже правило nullability по умолчанию было удалено.
Примечание. Правила правильности по умолчанию, упомянутые ниже, были удалены. Мы должны подтвердить, что это то, что мы хотим.
В разделе "Допустимость null" говорится:
Для типов объединения, в которых ни один из типов регистра не имеет значения NULL, состояние по умолчанию для
Valueпараметра "не равно NULL", а не "может быть null".
Учитывая, что в приведенном ниже примере текущая реализация считается Values2 "не null":
S2 s2 = default;
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => throw null!;
public S2(bool x) => throw null!;
object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
В то же время, раздел "Хорошо сформированность " говорит:
- Значение по умолчанию: если тип объединения является типом значения, значение по умолчанию имеет
nullзначение.Value- Конструктор по умолчанию: если тип объединения имеет конструктор nullary (no-argument), результирующий союз имеет
nullзначение как егоValue.
Реализация, как это, будет противоречить поведению анализа, допускающего значение NULL, в примере выше.
Следует ли настраивать правила правильности или иметь состояние Valuedefault "может быть null"?
Если последнее, следует инициализация S2 s2 = default; создать предупреждение о допустимости null?
Убедитесь, что параметр типа никогда не является типом объединения, даже если он ограничен одним.
class C1 : System.Runtime.CompilerServices.IUnion
{
private readonly object _value;
public C1(int x) { _value = x; }
public C1(string x) { _value = x; }
object System.Runtime.CompilerServices.IUnion.Value => _value;
}
class Program
{
static bool Test1<T>(T u) where T : C1
{
return u is int; // Not a union matching
}
static bool Test2<T>(T u) where T : C1
{
return u is string; // Not a union matching
}
}
Должны ли атрибуты после условия повлиять на допустимость по умолчанию экземпляра Union?
Примечание. Указанное ниже правило nullability по умолчанию было удалено. И мы больше не выводим значение NULL свойства Value из методов создания объединения. Таким образом, вопрос устарел или больше не применим к текущему дизайну.
Для типов объединения, в которых ни один из типов регистра не имеет значения NULL, состояние по умолчанию для
Valueпараметра "не равно NULL", а не "может быть null".
Ожидается предупреждение в следующем сценарии
#nullable enable
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null!;
public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!;
object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
class Program
{
static void Test2(S1 s)
{
// warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
// For example, the pattern 'null' is not covered.
_ = s switch { int => 1, bool => 3 }; //
}
}
Преобразования объединения
[Разрешено] Где они относятся к другим преобразованиям с приоритетом?
Преобразования объединения считаются другой формой определяемого пользователем преобразования. Поэтому текущая реализация классифицирует их сразу после неудачной попытки классифицировать неявное пользовательское преобразование, и в случае существования рассматривается как только другая форма определяемого пользователем преобразования. Это имеет следующие последствия:
- Неявное преобразование, определяемое пользователем, имеет приоритет над преобразованием объединения
- Если явное приведение используется в коде, явно определяемое пользователем преобразование имеет приоритет над преобразованием объединения.
- Если в коде нет явного приведения, преобразование объединения имеет приоритет над явным пользовательским преобразованием.
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => ...
public S1(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static implicit operator S1(int x) => ...
}
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(int x) => ...
public S2(string x) => ...
object System.Runtime.CompilerServices.IUnion.Value => ...
public static explicit operator S2(int x) => ...
}
class Program
{
static S1 Test1() => 10; // implicit operator S1(int x) is used
static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
static S2 Test3() => 10; // Union conversion S2.S2(int) is used
static S2 Test4() => (S2)20; // explicit operator S2(int x)
}
Необходимо подтвердить это поведение, которое нам нравится. В противном случае следует уточнить правила преобразования.
Резолюция:
Утвержден рабочей группой.
[Разрешено] Ref-ness параметра конструктора
В настоящее время язык разрешает только по значению и in параметрам для определяемых пользователем операторов преобразования.
Это чувствует, как причины этого ограничения также применимы к конструкторам, подходящим для преобразования объединения.
Предложение:
Измените определение приведенного case type constructorUnion types выше раздела:
-For each public constructor with exactly one parameter, the type of that parameter is considered a *case type* of the union type.
+For each public constructor with exactly one **by-value or `in`** parameter, the type of that parameter is considered a *case type* of the union type.
Резолюция:
Утверждена рабочей группой на данный момент. Однако мы можем рассмотреть возможность разделения набора конструкторов типов регистра и набора конструкторов, подходящих для преобразования типов объединения.
[Разрешено] Преобразования, допускающие значение NULL
Раздел преобразования, допускающие значение NULL, явно перечисляет преобразования, которые можно использовать в качестве базовых. Текущая спецификация не предлагает никаких изменений в этом списке. Это приведет к ошибке для следующего сценария:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1? Test1(int x)
{
return x; // error CS0029: Cannot implicitly convert type 'int' to 'S1?'
}
}
Предложение:
Измените спецификацию для поддержки неявного преобразования, допускающего значение NULL, из-за ST? преобразования объединения.
В частности, предполагается T , что тип объединения имеет неявное преобразование в тип из типа T? или выражения E , если есть преобразование объединения из E типа в тип C и C является типом регистра T.
Обратите внимание, что для типа E значения, не допускающего значение NULL, не требуется.
Преобразование оценивается как базовое преобразование объединения, за ST которым следует оболочка из TT?
Резолюция:
Утвержденных.
[Разрешено] Поднятые преобразования
Нужно ли изменить раздел " Поднятые преобразования" для поддержки поднятых преобразований объединения? В настоящее время они не допускаются:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(int x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(int? x)
{
return x; // error CS0029: Cannot implicitly convert type 'int?' to 'S1'
}
static S1? Test2(int? y)
{
return y; // error CS0029: Cannot implicitly convert type 'int?' to 'S1?'
}
}
Резолюция:
В настоящее время не было отменено преобразование профсоюзов. Некоторые заметки из обсуждения:
Аналогия с определяемым пользователем преобразованиям немного разбивается здесь. В общих профсоюзах может содержать значение NULL, которое приходится. Неясно, следует ли отменять создание экземпляра типа объединения со
nullзначением, хранящимся в нем, или следует ли создатьnullзначениеNullable<Union>.
[Разрешено] Блокировать преобразование объединения из экземпляра базового типа?
Можно найти текущее поведение, запутанное:
struct S1 : System.Runtime.CompilerServices.IUnion
{
public S1(System.ValueType x)
{
}
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(System.ValueType x)
{
return x; // Union conversion
}
static S1 Test2(System.ValueType y)
{
return (S1)y; // Unboxing conversion
}
}
Обратите внимание, что язык явно запрещает объявление определяемых пользователем преобразований из базового типа. Таким образом, это может сделать смысл, чтобы не разрешать преобразования профсоюзов, как это.
Резолюция:
Не делайте ничего особенного на данный момент. Универсальные сценарии не могут быть полностью защищены.
[Разрешено] Блокировать преобразование объединения из экземпляра типа интерфейса?
Можно найти текущее поведение, запутанное:
struct S1 : I1, System.Runtime.CompilerServices.IUnion
{
public S1(I1 x) => throw null;
public S1(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
interface I1 { }
struct S2 : System.Runtime.CompilerServices.IUnion
{
public S2(I1 x) => throw null;
public S2(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class C3 : System.Runtime.CompilerServices.IUnion
{
public C3(I1 x) => throw null;
public C3(string x) => throw null;
object System.Runtime.CompilerServices.IUnion.Value => throw null;
}
class Program
{
static S1 Test1(I1 x)
{
return x; // Union conversion
}
static S1 Test2(I1 x)
{
return (S1)x; // Unboxing
}
static S2 Test3(I1 x)
{
return x; // Union conversion
}
static S2 Test4(I1 x)
{
return (S2)x; // Union conversion
}
static C3 Test3(I1 x)
{
return x; // Union conversion
}
static C3 Test4(I1 x)
{
return (C3)x; // Reference conversion
}
}
Обратите внимание, что язык явно запрещает объявление определяемых пользователем преобразований из базового типа. Таким образом, это может сделать смысл, чтобы не разрешать преобразования профсоюзов, как это.
Резолюция:
Не делайте ничего особенного на данный момент. Универсальные сценарии не могут быть полностью защищены.
Пространство имен интерфейса IUnion
Содержащее пространство имен для IUnion интерфейса остается неопределенным. Если намерение состоит в том, чтобы сохранить его в global пространстве имен, давайте явно указываем.
Предложение: Если это что-то просто пропущенное, мы могли бы использовать System.Runtime.CompilerServices пространство имен.
Классы как Union типы
[Разрешено] Проверка самого экземпляра для null
Если тип объединения является типом класса, это значение может быть null. А как насчет проверок NULL?
Шаблон null был совместно проверен, чтобы проверить Value свойство, так как проверить, что само объединение не имеет значения NULL?
Рассмотрим пример.
- Когда
Sявляется структуройUnion, значениеS?имеетtrueзначение только в том случае,s is nullеслиsон самnull. КогдаCэтоUnionкласс,c is nullдля значенияfalseC?является то, когдаcсам являетсяnull, но этоtrueкогдаcсам неnullявляется иc.Valueявляетсяnull.
Еще один пример.
class C1 : IUnion
{
private readonly object? _value;
public C1(){}
public C1(int x) { _value = x; }
public C1(string x) { _value = x; }
object? IUnion.Value => _value;
}
class Program
{
static int Test1(C1? u)
{
// warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
// For example, the pattern 'null' is not covered.
// This is very confusing, the switch expression is indeed not exhaustive (u itself is not
// checked for null), but there is a case 'null => 3' in the switch expression.
// It looks like the only way to shut off the warning is to use 'case _'. Adding it removes
// all benefits of exhaustiveness checking, any union case could be missing and there would
// be no diagnostic about that.
return u switch { int => 1, string => 2, null => 3 };
}
}
Эта часть дизайна четко оптимизирована вокруг ожидания того, что тип объединения является структурой. Некоторые варианты:
- Очень плохо. Используйте
==для проверки null вместо совпадения шаблонов. - Пусть шаблон (и неявная
nullпроверка null в других шаблонах) применяется как к значению объединения, так и к егоValueсвойству:u is null ==> u == null || u.Value == null - Запретить классы быть типами объединения!
[Разрешено] Производный Union от класса
Когда класс использует Unionкласс в качестве базового класса в соответствии с текущей спецификацией, он становится самим классом Union. Это происходит, так как она автоматически наследует реализацию IUnion интерфейса, ее не требуется повторно реализовать. В то же время конструкторы производного типа определяют набор типов в этом новом.Union Очень легко добраться до очень странного поведения языка вокруг двух классов:
class C1 : IUnion
{
private readonly object _value;
public C1(long x) { _value = x; }
public C1(string x) { _value = x; }
object IUnion.Value => _value;
}
class C2(int x) : C1(x);
class Program
{
static int Test1(C1 u)
{
// Good
return u switch { long => 1, string => 2, null => 3 };
}
static int Test2(C2 u)
{
// error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'long'.
// error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'string'.
return u switch { long => 1, string => 2, null => 3 };
}
}
Некоторые варианты:
Изменение, когда тип класса является типом
Union. Например, класс является типомUnion, если все имеет значение true:- Это связано
sealedс тем, что производные типы не будут рассматриваться какUnionтипы, что позволяет запутать. - Ни одна из своих баз не реализует
IUnion
Это все еще не идеально. Правила слишком тонкие. Это легко сделать ошибку. В объявлении нет диагностики, но
Unionсопоставление не работает.- Это связано
Запретить классы от типа объединения.
[Разрешено] Оператор is-type
Оператор is-type указывается как проверка типа среды выполнения. Синтаксически это выглядит очень похоже на шаблон типа, но это не так. Поэтому не будет использоваться специальное Unionсопоставление, что может привести к путанице пользователя.
struct S1 : IUnion
{
private readonly object _value;
public S1(int x) { _value = x; }
public S1(string x) { _value = x; }
object IUnion.Value => _value;
}
class Program
{
static bool Test1(S1 u)
{
return u is int; // warning CS0184: The given expression is never of the provided ('int') type
}
static bool Test2(S1 u)
{
return u is string and ['1', .., '2']; // Good
}
}
В случае рекурсивного объединения шаблон типа может не дать никаких предупреждений, но он по-прежнему не будет делать то, что пользователь может думать, что это сделает.
Разрешение: Должен работать как шаблон типа.
Шаблон списка
Шаблон списка всегда завершается ошибкой при Union сопоставлении:
struct S1 : IUnion
{
private readonly object _value;
public S1(int[] x) { _value = x; }
public S1(string[] x) { _value = x; }
object IUnion.Value => _value;
}
class Program
{
static bool Test1(S1 u)
{
// error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found.
// error CS0021: Cannot apply indexing with [] to an expression of type 'object'
return u is [10];
}
}
static class Extensions
{
extension(object o)
{
public int Length => 0;
}
}
Другие вопросы
- При применении нескольких конструкторов в преобразованиях объединения и использовании
TryGetValue(...)сопоставления шаблонов объединения указываются как мягкие, когда применяются несколько конструкторов: они просто выбирают один. Это не должно иметь значения для правил хорошо сформированности, но мы комфортно с ним? - Спецификация не зависит от реализации
IUnion.Valueсвойства, а не любогоValueсвойства, найденного в самом типе объединения. Это предназначено для обеспечения большей гибкости для существующих типов (которые могут иметь собственноеValueсвойство для других использования) для реализации шаблона. Но это неловко, и несогласованно с тем, как другие члены найдены и используются непосредственно на типе объединения. Следует ли внести изменения? Другие варианты:- Требовать, чтобы типы профсоюзов предоставляли общедоступное
Valueсвойство. - Предпочитайте общедоступное
Valueсвойство, если оно существует, но вернитесь кIUnion.Valueреализации, если нет (аналогичноGetEnumeratorправилам).
- Требовать, чтобы типы профсоюзов предоставляли общедоступное
- Предлагаемый синтаксис объявления профсоюза не является универсальным, особенно когда дело доходит до выражения типов случаев. Альтернативы до сих пор также встречаются с критикой, но возможно, мы в конечном итоге в конечном итоге внося изменения. Некоторые основные опасения выразили о текущем:
- Запятые как разделители между типами регистров могут означать, что порядок имеет значение.
- Круглые списки выглядят слишком много, как первичные конструкторы (несмотря на отсутствие имен параметров).
- Слишком отличается от перечислений, которые имеют свои "дела" в фигурных скобках.
- Хотя объявления объединения создают структуры с одним полем ссылки, они по-прежнему подвержены неожиданному поведению при использовании в параллельном контексте. Например, если определяемый пользователем элемент функции разыменовывается
thisнесколько раз, то содержащая переменная может быть переназначен в целом другим потоком между двумя доступами. Компилятор может создать код для копированияthisв локальный при необходимости. Должно ли это? Как правило, какая степень устойчивости параллелизма является желательной и разумно достижимой?
C# feature specifications