Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Kwestia dotycząca mistrza: https://github.com/dotnet/csharplang/issues/9662
Podsumowanie
Związki to zestaw funkcji połączonych ze sobą, które łączą się w celu zapewnienia obsługi języka C# dla typów unii:
-
Typy unii: struktury i klasy, które mają
[Union]atrybut, są rozpoznawane jako typy unii i obsługują zachowania unii. - Typy przypadków: Typy unii mają zestaw typów przypadków, które są podawane przez parametry konstruktorom i metodom fabryki.
-
Zachowania unii: Typy unii obsługują następujące zachowania unii:
- Konwersje unii: istnieją niejawne konwersje unii z każdego typu przypadku na typ unii.
- Dopasowywanie unii: Dopasowywanie wzorca względem wartości unii niejawnie "odpakowuje" ich zawartość, stosując wzorzec do bazowej wartości.
- Wyczerpującość unii: wyrażenia przełącznika z wartościami unii są wyczerpujące, gdy wszystkie typy przypadków zostały dopasowane, bez konieczności tworzenia przypadku rezerwowego.
- Możliwość null unii: analiza wartości null ma ulepszone śledzenie stanu null zawartości unii.
- Wzorce unii: wszystkie typy unii są zgodne ze wzorcem podstawowym, ale istnieją dodatkowe opcjonalne wzorce dla określonych scenariuszy.
- Deklaracje unii: skrócona składnia umożliwia bezpośrednie deklarowanie typów unii. Implementacja jest "opiniowana" — deklaracja struktury zgodna ze wzorcem podstawowym unii i przechowuje zawartość jako jedno pole odwołania.
- Interfejsy unii: kilka interfejsów jest znanych przez język i używanych w implementacji deklaracji unii.
Motywacja
Związki to długotrwała funkcja języka C#, która umożliwia wyrażanie wartości z zamkniętego zestawu typów w sposób, w jaki dopasowanie wzorca może być wyczerpujące.
Separacja między typami unii a deklaracjami unii umożliwia języka C# posiadanie zwięzłej składni deklaracji unii z semantyką z opiniami, a jednocześnie umożliwia korzystanie z istniejących typów lub typów z innymi opcjami implementacji, aby wybrać zachowanie unii.
Proponowane związki zawodowe w języku C# to związki typów , a nie "dyskryminowane" lub "oznakowane". "Związki dyskryminujące" można wyrazić w kategoriach "związków typu" przy użyciu nowych deklaracji typów jako typów przypadków. Alternatywnie można je zaimplementować jako zamkniętą hierarchię, która jest kolejną, powiązaną, nadchodzącą funkcją języka C# skoncentrowaną na wyczerpującości.
Szczegółowy projekt
Typy unii
Każda klasa lub typ struktury z atrybutem System.Runtime.CompilerServices.UnionAttribute jest uważany za typ unii:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(Class | Struct, AllowMultiple = false)]
public class UnionAttribute : Attribute;
}
Typ unii musi być zgodny z określonym wzorcem członków unii publicznej, który musi być zadeklarowany na samym typie unii lub delegowany do "dostawcy składowych unii".
Niektórzy członkowie unii są obowiązkowi, a inne są opcjonalne.
Typ unii ma zestaw typów przypadków , które są ustanawiane na podstawie podpisów niektórych członków unii.
Dostęp do zawartości wartości unii można uzyskać za pośrednictwem Value właściwości. Język zakłada, że Value tylko kiedykolwiek zawiera wartość jednego z typów przypadków lub wartość null (zobacz Dobrze sformułowanie).
Dostawcy będący członkami unii
Domyślnie elementy członkowskie unii znajdują się w samym typie unii. Jeśli jednak typ unii bezpośrednio zawiera deklarację interfejsu o nazwie IUnionMembers , interfejs działa jako dostawca składowych unii. W takim przypadku członkowie unii znajdują się tylko u dostawcy składowych unii, a nie na samym typie unii.
Interfejs dostawcy składowych unii musi być publiczny, a sam typ unii musi zaimplementować go jako interfejs.
Używamy terminu typu definiującego unię dla typu, w którym znajdują się składowe unii: dostawca składowych unii, jeśli istnieje, i sam typ unii.
Członkowie unii
Składowe unii są sprawdzane według nazwy i podpisu w typie definiującym unię. Nie muszą być deklarowane bezpośrednio w typie definiującym unię, ale mogą być dziedziczone.
Jest to błąd, aby żaden członek unii nie był publiczny.
Składowe tworzenia i Value właściwość są obowiązkowe i są zbiorczo określane jako podstawowy wzorzec unii.
Składowe HasValue i TryGetValue są zbiorczo określane jako wzorzec dostępu do unii bez boksu.
W poniższych sposób opisano różne elementy członkowskie związku.
Członkowie tworzenia unii
Składowe tworzenia unii są używane do tworzenia nowych wartości unii na podstawie wartości typu sprawy.
Jeśli typ definiujący unię jest samym typem unii, każdy konstruktor z pojedynczym parametrem jest konstruktorem unii. Typy przypadków unii są identyfikowane jako zestaw typów utworzonych na podstawie typów parametrów tych konstruktorów w następujący sposób:
- Jeśli typ parametru jest typem dopuszczalnym wartości null (niezależnie od tego, czy jest to wartość, czy odwołanie), typ przypadku jest typem bazowym
- W przeciwnym razie typ przypadku jest typem parametru.
// 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) { ... }
Jeśli typ definiujący unię jest dostawcą składowych unii, każda metoda statyczna Create z pojedynczym parametrem i zwracanym typem, który jest konwertowany na tożsamość do samego typu unii, jest metodą fabryki unii.
Typy przypadków unii są identyfikowane jako zestaw typów utworzonych na podstawie typów parametrów tych metod fabrycznych w następujący sposób:
- Jeśli typ parametru jest typem dopuszczalnym wartości null (niezależnie od tego, czy jest to wartość, czy odwołanie), typ przypadku jest typem bazowym
- W przeciwnym razie typ przypadku jest typem parametru.
// 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) { ... }
Konstruktory unii i metody fabryki unii są określane zbiorczo jako składowe tworzenia unii.
Pojedynczy parametr elementu członkowskiego tworzenia unii musi być parametrem by-value lub in .
Typ unii musi mieć co najmniej jeden element członkowski tworzenia unii, a zatem co najmniej jeden typ przypadku.
Właściwość Value
Właściwość Value umożliwia dostęp do wartości zawartej w unii, niezależnie od typu wielkości liter.
Każdy typ definiujący unię musi zadeklarować Value właściwość typu object? lub object. Właściwość musi mieć metodę dostępu i opcjonalnie może mieć metodę getinit dostępu lub set , która może być dowolnego ułatwień dostępu i nie jest używana przez kompilator.
// Union 'Value' property
public object? Value { get; }
Elementy członkowskie dostępu bez boxingu
Typ unii może dodatkowo zaimplementować wzorzec dostępu unii bez pola, co umożliwia silnie typizowane dostęp warunkowy do każdego typu przypadku, a także sposób sprawdzania wartości null.
Dzięki temu kompilator może wydajniej implementować dopasowywanie wzorców, gdy typy przypadków są typami wartości i są przechowywane jako takie w unii.
Elementy członkowskie dostępu bez boksu to:
-
HasValueWłaściwość typuboolz publicznymgetakcesorem. Opcjonalnie może mieć metodęinitdostępu lubset, która może być dowolną dostępnością i nie jest używana przez kompilator. -
TryGetValueMetoda dla każdego typu przypadku. Metoda zwracabooli pobiera pojedynczy parametr out-parametr typu, który jest tożsamościami konwertowany na typ przypadku.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }
HasValue oczekuje się, że zwraca wartość true, jeśli i tylko wtedy, gdy związek Value nie ma wartości null.
TryGetValue oczekuje się, że zwraca wartość true, jeśli i tylko wtedy, gdy unii Value jest danego typu przypadku, a jeśli tak, podaj tę wartość w parametrze out metody.
Dobrze sformułowana
Język i kompilator tworzą szereg założeń behawioralnych dotyczących typów unii. Jeśli typ kwalifikuje się jako typ unii, ale nie spełnia tych założeń, zachowania unii mogą nie działać zgodnie z oczekiwaniami.
-
Dźwięk:
Valuewłaściwość zawsze oblicza wartość null lub wartość typu przypadku. Dotyczy to nawet wartości domyślnej typu unii. -
Stabilność: jeśli wartość unii zostanie utworzona na podstawie typu przypadku,
Valuewłaściwość będzie zgodna z typem lub wartością null. Jeśli wartość unii zostanie utworzona na podstawienullwartości,Valuewłaściwość będzie miećnullwartość . - Równoważność tworzenia: jeśli wartość jest niejawnie konwertowana na dwa różne typy przypadków, element członkowski tworzenia dla jednego z tych typów przypadków ma to samo zauważalne zachowanie, gdy jest wywoływane z tą wartością.
-
Spójność wzorca dostępu: zachowanie
HasValueTryGetValueelementów członkowskich dostępu innych niż boxing, jeśli są obecne, jest zauważalnie równoważne temu, czy sprawdzanie względemValuewłaściwości bezpośrednio.
Przykłady typów unii
Pet implementuje podstawowy wzorzec unii na samym typie unii:
[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 implementuje wzorzec dostępu bez boksu na samym typie unii:
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;
}
}
Uwaga: Jest to tylko przykład implementacji wzorca dostępu bez boksu. Kod użytkownika może przechowywać zawartość w dowolny sposób. W szczególności nie uniemożliwia to boksu implementacji! W non-boxing nazwie odwołuje się do umożliwienia implementacji dopasowania wzorca kompilatora w celu uzyskania dostępu do każdego typu przypadku w silnie typie, w przeciwieństwie do object?właściwości -typed Value .
Result<T> implementuje podstawowy wzorzec za pośrednictwem dostawcy składowych unii:
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;
}
Zachowania unii
Zachowania unii są zwykle implementowane za pomocą podstawowego wzorca unii. Jeśli związek oferuje wzorzec dostępu bez boksu, dopasowanie wzorca unii będzie preferencyjnie korzystać z niego.
Konwersje unii
Konwersja unii niejawnie konwertuje na typ unii z każdego z jego typów przypadków. W szczególności istnieje konwersja unii na typ U unii z typu lub wyrażeniaE, jeśli istnieje standardowa niejawna konwersja z E typu na typ C i C jest typem parametru elementu członkowskiego tworzenia unii .U
Jeśli typ U unii jest strukturą, istnieje konwersja unii na typ U? z typu lub wyrażeniaE, jeśli istnieje standardowa niejawna konwersja z E typu na typ C i C jest typem parametru elementu członkowskiego tworzenia unii .U
Konwersja unii nie jest standardową niejawną konwersją. W związku z tym nie może ona uczestniczyć w niejawnej konwersji zdefiniowanej przez użytkownika ani innej konwersji unii.
Nie ma jawnych konwersji unii poza niejawnymi konwersjami unii. W związku z tym, nawet jeśli istnieje jawna konwersja z E typu sprawy Cunii , to nie oznacza to, że istnieje jawna konwersja z E do tego typu unii.
Konwersja unii jest wykonywana przez wywołanie członka tworzenia unii:
Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");
Jest to błąd, jeśli rozwiązanie przeciążenia nie znajdzie jednego najlepszego członka kandydata lub jeśli ten członek nie jest jednym z członków unii typu unii.
Konwersja unii to po prostu kolejna "forma" niejawnej konwersji zdefiniowanej przez użytkownika. Odpowiedni operator konwersji zdefiniowany przez użytkownika "cienie" konwersji unii.
Uzasadnienie tej decyzji:
Jeśli ktoś napisał operator zdefiniowany przez użytkownika, powinien uzyskać priorytet. Innymi słowy, jeśli użytkownik faktycznie napisał własny operator, chce, abyśmy to nazwali. Istniejące typy z operatorami konwersji przekształcone w typy unii nadal działają tak samo w odniesieniu do istniejącego kodu korzystającego z operatorów dzisiaj.
W poniższym przykładzie niejawna konwersja zdefiniowana przez użytkownika ma pierwszeństwo przed konwersją unii.
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
}
W poniższym przykładzie, gdy jawne rzutowanie jest używane w kodzie, jawna konwersja zdefiniowana przez użytkownika ma pierwszeństwo przed konwersją unii. Jednak jeśli nie ma jawnego rzutu w kodzie, jest używana konwersja union, ponieważ jawna konwersja zdefiniowana przez użytkownika nie ma zastosowania.
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)
}
Dopasowywanie unii
Gdy wartość przychodząca wzorca jest typu unii lub dopuszczającego wartość null typu unii, wartość dopuszczana do wartości null i zawartość bazowej wartości unii może być "niezapisana", w zależności od wzorca.
W przypadku bezwarunkowych _ i var wzorców wzorzec jest stosowany do samej wartości przychodzącej. Przykład:
if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`
Jednak wszystkie inne wzorce są niejawnie stosowane do właściwości unii bazowej 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'
W przypadku wzorców logicznych ta reguła jest stosowana indywidualnie do gałęzi, mając na uwadze, że lewa gałąź and wzorca może mieć wpływ na typ przychodzący prawej gałęzi:
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?`.
}
Uwaga: Ta reguła oznacza, że GetPet() is Pet pet prawdopodobnie nie powiedzie się, podobnie jak Pet w przypadku zawartości, a nie samej Pet unii.
Uwaga: Powodem różnego traktowania bezwarunkowego var wzorca (a także _, który jest zasadniczo skrótem ) var _jest założenie, że ich stosowanie różni się jakościowo od innych wzorców.
var wzorce są używane po prostu do określania dopasowania wartości, często w zagnieżdżonych wzorcach, takich jak PetOwner{ Pet: var pet }. W tym miejscu przydatna semantyka służy pet do zachowania typu Petunii , zamiast Value właściwości, która jest wyłuszczana do typu bezużytecznego object? .
Jeśli wartość przychodząca jest typem klasy, wzorzec powiedzie się niezależnie od tego, null czy sama wartość unii jest null czy jej zawarta wartość to null:
if (result is null) { ... } // if (result == null || result.Value == null)
Inne wzorce dopasowania unii powiedzą się tylko wtedy, gdy sama wartość unii nie nulljest .
if (result is 1) { ... } // if (result != null && result.Value is 1)
Podobnie, jeśli wartość przychodząca jest typem wartości dopuszczających wartość null (zawijanie typu unii struktury), null wzorzec zakończy się powodzeniem niezależnie od tego, czy wartość przychodząca jest null , czy jej zawarta wartość to null:
if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)
Inne wzorce dopasowania unii powiedzą się tylko wtedy, gdy sama wartość przychodząca nie nulljest .
if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)
Kompilator preferuje implementowanie zachowania wzorca za pomocą elementów członkowskich przepisanych przez wzorzec dostępu bez boksu. Chociaż można wykonać dowolną optymalizację w granicach reguł dobrze sformułowanych, poniżej przedstawiono minimalny zestaw gwarantowany do zastosowania:
- W przypadku wzorca, który oznacza sprawdzenie określonego typu
T, jeśliTryGetValue(S value)metoda jest dostępna, i istnieje tożsamość lub niejawna konwersja odwołania/boxing zTdoS, to ta metoda jest używana do uzyskania wartości. Wzorzec jest następnie stosowany do tej wartości. Jeśli istnieje więcej niż jedna taka metoda, wówczas dowolna konwersja zTnaSnie jest preferowana, jeśli jest dostępna. Jeśli nadal istnieje więcej niż jedna metoda, jedna jest wybierana w sposób zdefiniowany przez implementację. - W przeciwnym razie w przypadku wzorca, który oznacza sprawdzenie
nullwartości , jeśliHasValuewłaściwość jest dostępna, ta właściwość jest używana do sprawdzania, czy wartość unii ma wartość null. - W przeciwnym razie wzorzec jest stosowany do wyniku uzyskiwania
IUnion.Valuedostępu do właściwości w unii przychodzącej.
Operator is-type zastosowany do typu unii ma takie samo znaczenie jak wzorzec typu zastosowany do typu unii.
Wyczerpującość unii
Przyjmuje się, że typ unii jest "wyczerpany" przez typy przypadków. Oznacza to, że wyrażenie jest wyczerpujące, switch jeśli obsługuje wszystkie typy przypadków unii:
var name = pet switch
{
Dog dog => ...,
Cat cat => ...,
// No warning about non-exhaustive switch
};
Możliwość przypisania wartości null
Stan null właściwości unii Value jest śledzony jak każda inna właściwość, z następującymi modyfikacjami:
- Gdy element członkowski tworzenia unii jest wywoływany (jawnie lub za pośrednictwem konwersji unii), nowa unia
Valuepobiera stan null wartości przychodzącej. - Jeśli wzorzec
HasValuedostępu bez pola lubTryGetValue(...)jest używany do wykonywania zapytań dotyczących zawartości typu unii (jawnie lub za pomocą dopasowywania wzorca), ma wpływ naValuestan null w taki sam sposób, jakValuew przypadku bezpośredniego sprawdzenia: stanValuenull elementu staje się "nie null" wtruegałęzi.
Nawet jeśli przełącznik unii jest w inny sposób wyczerpujący, jeśli stan null właściwości unii Value przychodzącej ma wartość "może null", ostrzeżenie zostanie podane na nieobsługiwanej wartości null.
Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
Dog dog => ...,
Cat cat => ...,
// Warning: 'null' not handled
}
Interfejsy unii
Następujące interfejsy są używane przez język w implementacji funkcji unii.
Interfejs dostępu unii
Interfejs IUnion oznacza typ jako typ unii w czasie kompilacji i zapewnia sposób uzyskiwania dostępu do zawartości unii w czasie wykonywania.
public interface IUnion
{
// The value of the union or null
object? Value { get; }
}
Związki generowane przez kompilator implementują ten interfejs.
Przykładowe użycie:
if (value is IUnion { Value: null }) { ... }
Deklaracje unii
Deklaracje unii są zwięzłym i opiniodawczym sposobem deklarowania typów unii w języku C#. Deklarują one strukturę, która używa pojedynczego odwołania do obiektu do przechowywania jego Value, co oznacza:
- Boxing: wszystkie typy wartości wśród ich typów przypadków zostaną wpisać w polu.
- Kompaktowanie: wartości unii zawierają tylko jedno pole.
Intencją jest deklaracje unii, aby obejmowały zdecydowaną większość przypadków użycia całkiem ładnie. Oczekuje się, że dwoma głównymi przyczynami kodowania ręcznego określonych typów unii, a nie deklaracjami unii:
- Dostosowanie istniejących typów do wzorców unii w celu uzyskania zachowań unii.
- Implementowanie innej strategii magazynowania z powodów, np. wydajności lub międzyoperacjonalności.
Składnia
Deklaracja unii ma nazwę i listę typów konstruktorów unii .
union_declaration
: attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
'(' type (',' type)* ')' struct_interfaces? type_parameter_constraints_clause*
(`{` struct_member_declaration* `}` | ';')
;
Oprócz ograniczeń dotyczących składowych struktury (§16.3) obowiązują następujące elementy:
- Pola wystąpień, zdarzenia automatyczne lub zdarzenia podobne do pól nie są dozwolone.
- Jawnie zadeklarowane konstruktory publiczne z pojedynczym parametrem nie są dozwolone.
- Jawnie zadeklarowane konstruktory muszą używać
this(...)inicjatora do delegowania (bezpośrednio lub pośrednio) do jednego z wygenerowanych konstruktorów.
Typy konstruktorów unii mogą być dowolnym typem, który konwertuje na object, np. interfejsy, parametry typu, typy dopuszczające wartość null i inne związki. W przypadku przypadków wynikowych nakładanie się na siebie i zagnieżdżanie związków lub ma wartość null.
Przykłady:
// 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){ ... }
Jest obniżana do:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public object? Value { get; }
... // original body
}
Otwórz pytania
[Rozwiązano] Czy deklaracja unii jest rekordem?
Deklaracja unii jest obniżana do struktury rekordów
Myślę, że to domyślne zachowanie jest niepotrzebne i, biorąc pod uwagę, że nie można go skonfigurować, znacznie ograniczy scenariusze użycia. Rekordy generują dużo kodu, który jest nieużywany lub nie spełnia określonych wymagań. Na przykład rekordy są prawie zabronione w bazie kodu kompilatora z powodu tego przeloowania kodu. Myślę, że lepiej byłoby zmienić wartość domyślną:
- Domyślnie deklaracja unii deklaruje regularną strukturę z elementami członkowskimi specyficznymi dla unii.
- Użytkownik może zadeklarować unię rekordów:
record union U(E1, ...) ...
Rozdzielczość: Deklaracja unii jest zwykłą strukturą, a nie strukturą rekordu. Element record union ... nie jest obsługiwany
[Rozwiązano] Składnia deklaracji unii
Wygląda na to, że proponowana składnia jest niekompletna lub niepotrzebnie ograniczana. Na przykład wygląda na to, że klauzula podstawowa nie jest dozwolona. Można jednak łatwo sobie wyobrazić potrzebę zaimplementowania interfejsu, na przykład.
Myślę, że oprócz listy typów elementów składnia powinna być zgodna z regularną struct/record struct deklaracjąunion, w której struct słowo kluczowe jest zastępowane słowem kluczowym.
Rozdzielczość: Ograniczenie zostało usunięte.
[Rozwiązano] Członkowie deklaracji unii
Pola wystąpień, zdarzenia automatyczne lub zdarzenia podobne do pól nie są dozwolone.
To czuje się arbitralnie i absolutnie niepotrzebne.
Rozdzielczość: Ograniczenie jest zachowywane.
[Rozwiązano] Typy wartości dopuszczalnych wartości jako typy przypadków unii
Typy przypadków unii są identyfikowane jako zestaw typów parametrów z tych konstruktorów. Typy przypadków unii są identyfikowane jako zestaw typów parametrów z tych metod fabrycznych.
Jednocześnie:
TryGetValueMetoda dla każdego typu przypadku. Metoda zwracabooli pobiera pojedynczy parametr wyjściowy typu, który odpowiada podanemu typowi przypadku w następujący sposób:
- Jeśli typ przypadku jest typem wartości dopuszczanej do wartości null, typ parametru powinien być konwertowany na tożsamość do bazowego typu
- W przeciwnym razie typ powinien być konwertowany na tożsamość do typu przypadku.
Czy istnieje zaleta typu wartości dopuszczanej do wartości null wśród typów przypadków, zwłaszcza że wzorzec typu nie może używać typu wartości dopuszczanej wartości null jako typu docelowego? Wygląda na to, że możemy po prostu powiedzieć, że jeśli typ parametru konstruktora/fabryki jest typem wartości dopuszczalnej wartości null, odpowiedni typ przypadku jest typem bazowym. Wtedy nie potrzebujemy tej dodatkowej TryGetValue klauzuli dla metody, wszystkie parametry out są typami przypadków.
Rozdzielczość: Sugestia została zatwierdzona
[Rozwiązano] Domyślny stan Value dopuszczania wartości null właściwości
W przypadku typów unii, w których żaden z typów przypadków nie może mieć wartości null, domyślny stan to
Value"nie null", a nie "może mieć wartość null".
W przypadku nowego projektu, gdzie Value właściwość nie jest zdefiniowana w interfejsie ogólnym, ale jest interfejsem API, który konkretnie należy do zadeklarowanego typu, reguła cytowana powyżej czuje się jak nadmierna inżynieria. Ponadto reguła prawdopodobnie zmusi konsumentów do używania typów dopuszczanych do wartości null w sytuacjach, w których w przeciwnym razie typy dopuszczane do wartości null nie będą używane.
Rozważmy na przykład następującą deklarację unii:
union U1(int, bool, DateTime);
Zgodnie z cytowaną regułą domyślny stan dla Value parametru to "not null". Ale to nie pasuje do zachowania typu, default(U1).Value to null. Aby realizować zachowanie, użytkownik jest zmuszony do tworzenia co najmniej jednego typu przypadku dopuszcza wartość null. Coś takiego jak:
union U1(int?, bool, DateTime);
Ale jest to prawdopodobnie niepożądane, konsument może nie chcieć zezwolić na jawne tworzenie z wartością int? .
Propozycja: Usuń regułę cytowaną, analiza dopuszczana do wartości null powinna używać adnotacji z Value właściwości, aby wywnioskować jego domyślną wartość null.
Rozdzielczość: Wniosek zostanie zatwierdzony
[Rozwiązano] Dopasowywanie unii dla wartości null typu wartości unii
Gdy wartość przychodząca wzorca jest typu unii, zawartość wartości unii może być "rozpasana", w zależności od wzorca.
Czy należy rozszerzyć tę regułę na scenariusze, gdy wartość przychodząca wzorca ma wartość Nullable<union type>?
Rozważmy następujący scenariusz:
static bool Test1(StructUnion? u)
{
return u is 1;
}
static bool Test2(ClassUnion? u)
{
return u is 1;
}
Znaczenie w u is 1 testach Test1 i Test2 jest bardzo różne. W test1 nie jest to dopasowanie unii, w teście2.
Być może "dopasowanie unii" powinno "kopać" przez Nullable<T> jako dopasowanie wzorca zwykle w innych sytuacjach.
Jeśli pójdziemy z tym, wówczas wzorzec dopasowania null unii względem Nullable<union type> powinien działać tak jak w przypadku klas.
Tj. wzorzec ma wartość true, gdy (!nullableValue.HasValue || nullableValue.Value.Value is null).
Rozdzielczość: Wniosek zostanie zatwierdzony.
Co zrobić z "złymi" interfejsami API?
Co kompilator powinien zrobić w przypadku interfejsów API pasujących do unii, które wyglądają jak dopasowanie, ale w przeciwnym razie "złe"? Na przykład kompilator znajduje element TryGetValue/HasValue z pasującym podpisem, ale jest on "nieprawidłowy", ponieważ wymagany modyfikator niestandardowy lub wymaga nieznanej funkcji itp. Czy kompilator powinien dyskretnie ignorować interfejs API lub zgłaszać błąd? Podobnie interfejs API może być oznaczony jako przestarzały/eksperymentalny. Czy kompilator powinien zgłaszać jakiekolwiek dane diagnostyczne, dyskretnie używać interfejsu API lub dyskretnie nie używać interfejsu API?
Brak typów if deklaracji unii
Co się stanie, jeśli UnionAttributebrakuje elementu lub IUnionIUnion<TUnion> ? Błąd? Syntezy? Coś innego?
[Rozwiązano] Projektowanie ogólnego interfejsu IUnion
Argumenty zostały wykonane, które IUnion<TUnion> nie powinny dziedziczyć z IUnion lub ograniczać jego parametr typu do IUnion<TUnion>. Powinniśmy ponownie wrócić.
Rozdzielczość: Interfejs IUnion<TUnion> zostanie usunięty na razie.
[Rozwiązano] Typy wartości dopuszczalnych wartości jako typy liter i ich interakcja z TryGetValue
Powyższe reguły stwierdzają, że jeśli typ przypadku jest typem wartości dopuszczanej do wartości null, typ parametru używany w odpowiedniej TryGetValue metodzie powinien być typem bazowym .
Jest to motywowane faktem, że null wartość nigdy nie zostanie zwrócona za pomocą tej metody. Po stronie zużycia typ wartości dopuszczanej do wartości null jest niedozwolony jako wzorzec typu, natomiast dopasowanie względem typu bazowego powinno być możliwe mapowania na wywołanie tej metody.
Powinniśmy potwierdzić, że zgadzamy się z tym rozpakiem.
Rozdzielczość: Uzgodnione/potwierdzone
Wzorzec dostępu do unii bez boksu
Należy określić dokładne reguły znajdowania odpowiednich HasValue interfejsów API i TryGetValue .
Czy dotyczy dziedziczenia? Czy odczyt/zapis HasValue jest zgodny z akceptowalnym dopasowaniem? itp.
[Rozwiązano] TryGetValue pasujące konwersje
Sekcja Dopasowywanie unii mówi:
W przypadku wzorca, który oznacza sprawdzenie określonego typu
T, jeśliTryGetValue(S value)metoda jest dostępna, i istnieje niejawna konwersja zTnaS, to ta metoda jest używana do uzyskania wartości.
Czy zestaw niejawnych konwersji jest ograniczony w jakikolwiek sposób? Na przykład czy dozwolone są konwersje zdefiniowane przez użytkownika? A co z konwersjami krotki i innymi nie tak trywialnymi konwersjami? Niektóre z nich to nawet standardowe konwersje.
Czy zestaw TryGetValue metod jest ograniczony w jakikolwiek inny sposób? Na przykład sekcja Wzorce unii oznacza, że uwzględniane są tylko metody z typem parametru pasującym do typu przypadku:
metoda
public bool TryGetValue(out T value)dla każdego typuTprzypadku .
Dobrze byłoby mieć jawną odpowiedź.
Rozdzielczość: Uwzględniane są tylko niejawne tożsamości, odwołania lub konwersje boksu
TryGetValue i analiza dopuszczana do wartości null
Jeśli wzorzec
HasValuedostępu bez pola lubTryGetValue(...)jest używany do wykonywania zapytań dotyczących zawartości typu unii (jawnie lub za pomocą dopasowywania wzorca), ma wpływ naValuestan null w taki sam sposób, jakValuew przypadku bezpośredniego sprawdzenia: stanValuenull elementu staje się "nie null" wtruegałęzi.
Czy zestaw TryGetValue metod jest ograniczony w jakikolwiek sposób? Na przykład sekcja Wzorce unii oznacza, że uwzględniane są tylko metody z typem parametru pasującym do typu przypadku:
metoda
public bool TryGetValue(out T value)dla każdego typuTprzypadku .
Dobrze byłoby mieć jawną odpowiedź.
Wyjaśnienie reguł dotyczących default wartości typów unii struktury
Uwaga: domyślna reguła dopuszczania wartości null wymieniona poniżej została usunięta.
Uwaga: usunięto reguły prawidłowo sformułowania "domyślne" wymienione poniżej. Powinniśmy potwierdzić, że jest to to, czego chcemy.
Sekcja dopuszczania wartości null mówi:
W przypadku typów unii, w których żaden z typów przypadków nie może mieć wartości null, domyślny stan to
Value"nie null", a nie "może mieć wartość null".
Biorąc pod uwagę, że w poniższym przykładzie bieżąca implementacja s2 jest uważana za Value "nie 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!;
}
W tym samym czasie sekcja Dobrze sformułowana mówi:
- Wartość domyślna: jeśli typ unii jest typem wartości, wartość domyślna ma
nullwartość .Value- Konstruktor domyślny: jeśli typ unii ma konstruktor null (no-argument), wynikowy związek ma
nulljako .Value
Implementacja taka będzie sprzeczna z zachowaniem analizy dopuszczalnej do wartości null w powyższym przykładzie.
Czy reguły dobrze sformułowania powinny być dostosowywane lub czy powinien mieć stan Valuedefault "może mieć wartość null"?
Jeśli to drugie, czy inicjowanie S2 s2 = default; powinno spowodować wyświetlenie ostrzeżenia o wartości null?
Upewnij się, że parametr typu nigdy nie jest typem unii, nawet jeśli jest ograniczony do jednego.
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
}
}
Czy atrybuty po warunku mają wpływ na domyślną wartość null wystąpienia unii?
Uwaga: domyślna reguła dopuszczania wartości null wymieniona poniżej została usunięta. Nie wywnioskujemy już domyślnej Value wartości null właściwości z metod tworzenia unii. W związku z tym pytanie jest przestarzałe/nie dotyczy już bieżącego projektu.
W przypadku typów unii, w których żaden z typów przypadków nie może mieć wartości null, domyślny stan to
Value"nie null", a nie "może mieć wartość null".
Czy ostrzeżenie jest oczekiwane w poniższym scenariuszu
#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 }; //
}
}
Konwersje unii
[Rozwiązano] Gdzie należą one między innymi konwersjami priorytetowo?
Konwersje unii czują się jak inna forma konwersji zdefiniowanej przez użytkownika. W związku z tym bieżąca implementacja klasyfikuje je bezpośrednio po nieudanej próbie sklasyfikowania niejawnej konwersji zdefiniowanej przez użytkownika, a w przypadku istnienia jest traktowana jako tylko inna forma konwersji zdefiniowanej przez użytkownika. Ma to następujące konsekwencje:
- Niejawna konwersja zdefiniowana przez użytkownika ma pierwszeństwo przed konwersją unii
- Gdy jawne rzutowanie jest używane w kodzie, jawna konwersja zdefiniowana przez użytkownika ma pierwszeństwo przed konwersją unii
- Jeśli w kodzie nie ma jawnego rzutu, konwersja unii ma priorytet nad jawną konwersją zdefiniowaną przez użytkownika
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)
}
Należy potwierdzić, że jest to zachowanie, które lubimy. W przeciwnym razie należy wyjaśnić reguły konwersji.
Rozwiązanie:
Zatwierdzone przez grupę roboczą.
[Rozwiązano] Ref-ness parametru konstruktora
Obecnie język zezwala tylko na parametry i in wartości dla operatorów konwersji zdefiniowanych przez użytkownika.
Wydaje się, że przyczyny tego ograniczenia mają również zastosowanie do konstruktorów odpowiednich do konwersji union.
Wniosek:
Dostosuj definicję elementu case type constructor w Union types sekcji powyżej:
-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.
Rozwiązanie:
Na razie zatwierdzona przez grupę roboczą. Możemy jednak rozważyć "podzielenie" zestawu konstruktorów typu wielkości liter i zestawu konstruktorów odpowiednich do konwersji typów unii.
[Rozwiązano] Konwersje dopuszczane do wartości null
Sekcja Konwersje dopuszczające wartości null jawnie wyświetla konwersje, które mogą być używane jako bazowe. Bieżąca specyfikacja nie proponuje żadnych korekt tej listy. Spowoduje to błąd dla następującego scenariusza:
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?'
}
}
Wniosek:
Dostosuj specyfikację, aby obsługiwać niejawną konwersję dopuszczaną do wartości null z S do T? wspieranej przez konwersję unii.
W szczególności przy założeniu, że T typ unii jest niejawną konwersją na typ T? z typu lub wyrażenia E , jeśli istnieje konwersja unii z E typu na typ C i C jest typem Twielkości liter .
Należy pamiętać, że nie ma potrzeby, aby typ E wartości był typem wartości nienależących do wartości null.
Konwersja jest oceniana jako konwersja unii bazowej z S do, po której następuje zawijanie od T do TT?
Rozwiązanie:
Zatwierdzone.
[Rozwiązano] Konwersje zniesione
Czy chcemy dostosować sekcję Konwersje zniesione , aby obsługiwać zniesione konwersje union? Obecnie są one niedozwolone:
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?'
}
}
Rozwiązanie:
Na razie nie zniesione konwersje unii. Niektóre uwagi z dyskusji:
Analogia do konwersji zdefiniowanych przez użytkownika nieco się tutaj rozkłada. Ogólnie rzecz biorąc, związki mogą zawierać wartość null, która jest w. Nie jest jasne, czy podnoszenie powinno utworzyć wystąpienie typu unii z wartością
nullprzechowywaną w nim, czy też utworzyćnullwartośćNullable<Union>.
[Rozwiązano] Czy zablokować konwersję unii z wystąpienia typu podstawowego?
Może się okazać, że bieżące zachowanie jest mylące:
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
}
}
Należy pamiętać, że język jawnie nie zezwala na deklarowanie konwersji zdefiniowanych przez użytkownika z typu podstawowego. W związku z tym może to oznaczać, że nie zezwala na takie konwersje unii.
Rozwiązanie:
Nie rób na razie nic specjalnego. Scenariusze ogólne nie mogą być w pełni chronione.
[Rozwiązano] Czy zablokować konwersję unii z wystąpienia typu interfejsu?
Może się okazać, że bieżące zachowanie jest mylące:
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
}
}
Należy pamiętać, że język jawnie nie zezwala na deklarowanie konwersji zdefiniowanych przez użytkownika z typu podstawowego. W związku z tym może to oznaczać, że nie zezwala na takie konwersje unii.
Rozwiązanie:
Nie rób na razie nic specjalnego. Scenariusze ogólne nie mogą być w pełni chronione.
Przestrzeń nazw interfejsu IUnion
Zawieranie przestrzeni nazw dla IUnion interfejsu pozostaje nieokreślone. Jeśli intencją jest przechowywanie jej w global przestrzeni nazw, określmy to jawnie.
Propozycja: Jeśli jest to coś po prostu pominiętego, możemy użyć System.Runtime.CompilerServices przestrzeni nazw.
Klasy jako Union typy
[Rozwiązano] Sprawdzanie samego wystąpienia dla null
Jeśli typ unii jest typem klasy, wartość może mieć wartość null. Co z sprawdzaniem wartości null?
Wzorzec null został co-opted w celu sprawdzenia Value właściwości, więc jak sprawdzić, czy sam związek nie ma wartości null?
Przykład:
- Gdy
Sjest strukturąUnion, dla wartościS?jest tylko wtedy,s is nullgdyssama jesttruenull. GdyCjest klasąUnion,c is nulldla wartościC?jestfalse, gdycsama jestnull, ale jesttrue, gdycsama nienulljest ic.Valuejestnull.
Inny przykład:
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 };
}
}
Ta część projektu jest wyraźnie zoptymalizowana pod kątem oczekiwania, że typ unii jest strukturą. Niektóre opcje:
- Szkoda. Użyj
==dla sprawdzania wartości null zamiast dopasowania wzorca. - Niech
nullwzorzec (i niejawne sprawdzanie wartości null w innych wzorcach) ma zastosowanie zarówno do wartości unii, jak i jejValuewłaściwości:u is null ==> u == null || u.Value == null. - Nie zezwalaj klasom na bycie typami unii!
[Rozwiązano] Wyprowadzanie z Union klasy
Gdy klasa używa Unionklasy jako klasy bazowej, zgodnie z bieżącą specyfikacją, staje się samą klasą Union. Dzieje się tak, ponieważ automatycznie "dziedziczy" implementację interfejsu, nie jest wymagana do jej ponownego IUnion zaimplementowania. Jednocześnie konstruktory typu pochodnego definiują zestaw typów w tym nowym obiekcie Union. Bardzo łatwo jest uzyskać bardzo dziwne zachowanie języka wokół dwóch klas:
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 };
}
}
Niektóre opcje:
Zmień, gdy typ klasy jest typem
Union. Na przykład klasa jest typemUnion, gdy wszystko ma wartość true:- Wynika
sealedto z faktu, że typy pochodne nie będą traktowane jakoUniontypy, co pozwala na mylące. - Żadna z jego baz nie implementuje
IUnion
To nadal nie jest idealne. Reguły są zbyt subtelne. Łatwo jest popełnić błąd. Nie ma diagnostyki deklaracji, ale
Uniondopasowanie nie działa.- Wynika
Nie zezwalaj klasom na bycie typami unii.
[Rozwiązano] Operator typu is
Operator is-type jest określony jako sprawdzanie typu środowiska uruchomieniowego. Składniowo wygląda bardzo podobnie do wzorca typu, ale tak nie jest. W związku z tym nie będzie używane specjalne Uniondopasowanie, co może prowadzić do nieporozumień użytkownika.
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
}
}
W przypadku rekursywnego związku wzorzec typu może nie zawierać ostrzeżenia, ale nadal nie zrobi tego, co użytkownik może myśleć, że zrobi.
Rozdzielczość: Powinna działać jako wzorzec typu.
Wzorzec listy
Wzorzec listy zawsze kończy się niepowodzeniem z Union dopasowaniem:
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;
}
}
Inne pytania
- Zarówno użycie konstruktorów w konwersjach unii, jak i użycie
TryGetValue(...)w dopasowywaniu wzorca unii jest określone jako łagodne, gdy stosuje się wiele konstruktorów: po prostu wybierzą jeden. Nie powinno to mieć znaczenia zgodnie z dobrze sformułowanymi zasadami, ale czy dobrze się z tym czujemy? - Specyfikacja subtelnie opiera się na implementacji
IUnion.Valuewłaściwości, a nie dowolnejValuewłaściwości znalezionej na samym typie unii. Ma to na celu zapewnienie większej elastyczności dla istniejących typów (które mogą mieć własnąValuewłaściwość dla innych zastosowań) w celu zaimplementowania wzorca. Ale jest to niezręczne i niezgodne z tym, jak inne elementy członkowskie są znajdowane i używane bezpośrednio w typie unii. Czy powinniśmy wprowadzić zmianę? Niektóre inne opcje:- Wymagaj, aby typy unii uwidoczniły właściwość publiczną
Value. - Preferuj właściwość publiczną
Value, jeśli istnieje, ale wróć doIUnion.Valueimplementacji, jeśli nie (podobnie jakGetEnumeratorw przypadku reguł).
- Wymagaj, aby typy unii uwidoczniły właściwość publiczną
- Proponowana składnia deklaracji unii nie jest powszechnie kochana, szczególnie w przypadku wyrażania typów przypadków. Alternatywy do tej pory spotykają się również z krytyką, ale jest możliwe, że w końcu wprowadzimy zmianę. Niektóre najważniejsze obawy wyraziły się na temat obecnego:
- Przecinki jako separatory między typami liter mogą sugerować, że kolejność ma znaczenie.
- Listy nawiasów wyglądają zbytnio jak konstruktory podstawowe (pomimo braku nazw parametrów).
- Zbyt różni się od wyliczenia, które mają swoje "przypadki" w nawiasach klamrowych.
- Chociaż deklaracje unii generują struktury z pojedynczym polem odniesienia, są one nadal nieco podatne na nieoczekiwane zachowanie w przypadku użycia w kontekście współbieżnym. Jeśli na przykład element członkowski funkcji zdefiniowany przez użytkownika wyłuszczy
thiswięcej niż raz, zmienna zawierająca mogła zostać ponownie przypisana jako całość przez inny wątek między dwoma dostępami. Kompilator może wygenerować kod do skopiowaniathisdo lokalnego w razie potrzeby. Czy powinno? Ogólnie rzecz biorąc, jaki stopień odporności współbieżności jest pożądany i rozsądnie osiągalny?
C# feature specifications