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.
Typ unii reprezentuje wartość, która może być jednym z kilku typów przypadków. Związki zapewniają niejawne konwersje z każdego typu przypadku, wyczerpujące dopasowanie wzorca i ulepszone śledzenie wartości null. Użyj słowa kluczowego union , aby zadeklarować typ unii:
public union Pet(Cat, Dog, Bird);
Ta deklaracja tworzy unię z trzema Pet typami przypadków: Cat, Dog, i Bird. Możesz przypisać dowolną wartość typu przypadku do zmiennej Pet . Kompilator zapewnia, że switch wyrażenia obejmują wszystkie typy przypadków.
Dokumentacja języka C# zawiera ostatnio wydaną wersję języka C#. Zawiera również początkową dokumentację dla funkcjonalności w publicznych wersjach testowych nadchodzącego wydania języka.
Dokumentacja identyfikuje dowolną funkcję po raz pierwszy wprowadzoną w ostatnich trzech wersjach języka lub w bieżącej publicznej wersji zapoznawczej.
Wskazówka
Aby dowiedzieć się, kiedy funkcja została po raz pierwszy wprowadzona w języku C#, zapoznaj się z artykułem dotyczącym historii wersji języka C#.
Zadeklaruj unię, gdy wartość musi być dokładnie jednym ze stałych zestawów typów i chcesz, aby kompilator wymuszał obsługę każdej możliwości. Typowe scenariusze obejmują:
-
Zwraca wynik lub błąd: metoda zwraca wartość powodzenia lub wartość błędu, a obiekt wywołujący musi obsłużyć obie te wartości. Związek taki jak
union Result(Success, Error)sprawia, że zestaw wyników jest jawny. -
Wysyłanie komunikatów lub poleceń: system przetwarza zamknięty zestaw typów komunikatów. Związek zapewnia, że nowe typy komunikatów generują ostrzeżenia w czasie kompilacji na każdym
switch, który jeszcze ich nie obsługuje. - Zastępowanie interfejsów znaczników lub abstrakcyjnych klas bazowych: jeśli używasz interfejsu lub klasy abstrakcyjnej wyłącznie do grupowania typów do dopasowywania wzorców, unia zapewnia wyczerpujące sprawdzanie bez konieczności dziedziczenia lub współużytkowanych elementów członkowskich.
Związek różni się od innych deklaracji typów w ważny sposób:
- W przeciwieństwie do elementu
classlubstruct, unia nie definiuje nowych składowych danych. Zamiast tego komponuje istniejące typy do zamkniętego zestawu alternatyw. -
interfaceW przeciwieństwie do elementu , unia jest zamknięta — należy zdefiniować pełną listę typów przypadków w deklaracji, a kompilator używa tej listy do sprawdzania wyczerpującości. -
recordW przeciwieństwie do elementu , unia nie dodaje równości, klonowania ani dekonstrukcji zachowania. Związek koncentruje się na "którym przypadku jest?" zamiast "jakie pola ma?"
Deklaracje unii
Deklaracja unii określa nazwę i listę typów przypadków:
public union Pet(Cat, Dog, Bird);
Typy przypadków mogą być dowolnym typem, który konwertuje na object, w tym klasy, struktury, interfejsy, parametry typu, typy dopuszczające wartość null i inne związki. W poniższych przykładach przedstawiono różne możliwości typów przypadków:
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
Gdy typ przypadku jest typem wartości (na przykład int), wartość jest w polu przechowywana we właściwości unii Value . Związki zawodowe przechowują swoją zawartość jako pojedyncze object? odwołanie.
Deklaracja unii może zawierać treść z dodatkowymi członkami, podobnie jak struktura, z zastrzeżeniem pewnych ograniczeń. Deklaracje unii nie mogą zawierać pól wystąpień, właściwości automatycznych ani zdarzeń podobnych do pól. Nie można również zadeklarować publicznych konstruktorów za pomocą jednego parametru, ponieważ kompilator generuje te konstruktory jako składowe tworzenia unii. Poniższa Length unia dodaje TotalMeters właściwość, która używa dopasowania wzorca do obsługi każdego typu przypadku wraz z metodą łączącą Add dwie długości:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
Konwersje unii
Niejawna konwersja unii istnieje z każdego typu przypadku do typu unii:
static void BasicConversion()
{
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
Konwersje unii działają przez wywołanie odpowiedniego wygenerowanego konstruktora. Jeśli operator konwersji niejawnej zdefiniowanej przez użytkownika istnieje dla tego samego typu, operator zdefiniowany przez użytkownika ma pierwszeństwo przed konwersją unii. Jeśli do wartości źródłowej ma zastosowanie więcej niż jeden typ przypadku, konwersja unii jest niejednoznaczna, a kompilator zgłasza błąd. Aby uzyskać szczegółowe informacje na temat priorytetu konwersji, zobacz specyfikację funkcji.
Konwersja unii do struktury unii dopuszczanej do wartości null (T?) działa również wtedy, gdy T jest typem unii:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Dog: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
Dopasowywanie wzorca unii
Gdy wzorzec jest zgodny z typem unii, wzorce są zwykle stosowane do właściwości unii Value , a nie samej wartości unii. To zachowanie "rozpakuj" oznacza, że związek jest niewidoczny dla dopasowania wzorca:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Trzy wzorce są wyjątkami od tej reguły: wzorzec odrzucenia _ , var wzorzec i not wzorzec mają zastosowanie do samej wartości unii, a nie jej Value właściwości. Użyj var polecenia , aby przechwycić wartość unii w przypadku GetPet() zwracania Pet? wartości (Nullable<Pet>):
if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
W wzorcach logicznych każda gałąź jest zgodna z regułą rozpasywania osobno. Lewa gałąź and wzorca może zmienić wartość przychodzącą widoczną w prawej gałęzi.
not Ponieważ wzorzec ma zastosowanie do wartości unii przychodzącej, a nie jej Value, wiodące not null nie opakowuje wartości gałęzi, która następuje po niej:
GetPet() switch
{
// 'var pet' captures the Pet?; 'not null' applies to the Pet? value (not pet.Value)
var pet and not null => ...,
// 'not null' doesn't unwrap to Pet, so 'var value' still captures the Pet?
not null and var value => ...,
}
Uwaga / Notatka
Ponieważ wzorce mają zastosowanie do Valuemetody , wzorzec, taki jak pet is Pet zwykle nie jest zgodny, ponieważ Pet jest testowany pod kątem zawartości unii, a nie samej unii.
Dopasowywanie wartości null
W przypadku związków struktur wzorzec sprawdza, null czy Value ma wartość null:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
}
W przypadku związków opartych na klasach następuje powodzenie, null gdy odwołanie do unii ma wartość null lub jego Value właściwość ma wartość null:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
W przypadku typów struktury unii dopuszczanej do wartości null (Pet?) następuje powodzenie, null gdy otoka dopuszczana do wartości null lub gdy bazowej unii Value ma wartość null.
Wyczerpującość unii
Wyrażenie switch jest wyczerpujące, gdy obsługuje wszystkie typy przypadków unii. Kompilator ostrzega tylko wtedy, gdy typ sprawy nie jest obsługiwany. Nie musisz uwzględniać wzorca odrzucenia (_) ani var wzorca, aby dopasować dowolny typ, gdy wyrażenie jest zdecydowanie przypisane:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Jeśli stan null właściwości unii Value to "może null", należy również obsłużyć null , aby uniknąć ostrzeżenia:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
}
Taka sytuacja może wystąpić, gdy union wyrażenie jest wartością domyślną lub nie jest zdecydowanie przypisane, jak pokazano w poprzednim przykładzie.
Możliwość przypisania wartości null
Kompilator śledzi stan null właściwości unii Value za pomocą następujących reguł:
- Domyślny stan null właściwości unii
Valueto "może null", jeśli domyślnym stanem null każdego typu przypadku jest "może null". W przeciwnym razie domyślny stan null to "nie null". - Podczas tworzenia wartości unii na podstawie typu przypadku (za pomocą konstruktora lub konwersji unii)
Valuepobiera stan null wartości przychodzącej. - Gdy wzorzec
HasValuedostępu innego niż boxing lubTryGetValue(...)elementy członkowskie wysyłają zapytanie do zawartości unii, stanValuenull staje się "nie null" wtruegałęzi.
Niestandardowe typy związków
Kompilator konwertuje deklarację union na deklarację struct . Struktura jest oznaczona atrybutem [System.Runtime.CompilerServices.Union] i implementuje IUnion interfejs. Zawiera on publiczny konstruktor i niejawną konwersję dla każdego typu przypadku wraz z właściwością Value . Ta wygenerowana forma jest uważana. Zawsze jest to struktura, zawsze pola przypadków typu wartości i zawsze przechowuje zawartość jako object?.
Może być konieczne inne zachowanie, jeśli chcesz dostosować istniejący typ, utworzyć unię opartą na klasach lub użyć niestandardowej strategii magazynowania lub jeśli potrzebujesz obsługi międzyoperacyjnej. Typ unii można utworzyć ręcznie.
Każda klasa lub struktura z atrybutem jest [Union], jeśli jest zgodny ze wzorcem podstawowym unii. Podstawowy wzorzec unii wymaga:
- Atrybut
[Union]typu. - Co najmniej jeden publiczny konstruktor, z których każdy ma jedną wartość
inlub parametr. Typ parametru każdego konstruktora definiuje typ przypadku. - Właściwość publiczna
Valuetypuobject?(lubobject) z akcesoremget.
Wszyscy poprzedni członkowie związku muszą być publiczne. Kompilator używa tych elementów członkowskich do implementowania konwersji unii, dopasowywania wzorców i sprawdzania kompletności. Można również zaimplementować wzorzec dostępu bez boxingu lub utworzyć typ unii opartej na klasach. Niestandardowy typ unii może dodawać dodatkowe elementy członkowskie.
Kompilator zakłada, że niestandardowe typy unii spełniają następujące reguły zachowania:
-
Dźwięk:
Valuezawsze zwracanulllub wartość jednego z typów przypadków — nigdy nie jest to wartość innego typu. W przypadku związkówdefaultstruktur tworzy elementValueo wartościnull. -
Stabilność: jeśli tworzysz wartość unii na podstawie typu sprawy,
Valuepasuje do tego typu przypadku (lub jest tonull, czy dane wejściowe tonull). - Równoważność tworzenia: jeśli wartość jest niejawnie konwertowana na dwa różne typy przypadków, oba elementy członkowskie tworzenia tworzą to samo zauważalne zachowanie.
-
Spójność wzorca dostępu:
HasValueelementy członkowskie iTryGetValue, jeśli są obecne, zachowują się równoważnie do bezpośredniego sprawdzaniaValue.
W poniższym przykładzie pokazano niestandardowy typ unii:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
Wzorzec dostępu bez boksu
Niestandardowy typ unii może opcjonalnie zaimplementować wzorzec dostępu bez użycia pola, aby umożliwić silnie typizowane przypadki typu wartości bez boksowania podczas dopasowywania wzorca. Ten wzorzec wymaga:
- Właściwość
HasValuetypubool, która zwracatruewartość , gdyValuenienulljest . -
TryGetValueMetoda dla każdego typu przypadku, który zwracabooli dostarcza wartość za pośrednictwem parametruout.TryGetValueZwracatruewartość tylko wtedy, gdyValuejest wartością inną niż null tego typu przypadku.outTyp parametru to identity-cabrio do typu przypadku lub do bazowego typu wartości, gdy typ przypadku jest typem wartości dopuszczanej wartości null.
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
Kompilator preferuje TryGetValue nad właściwością Value podczas implementowania dopasowywania wzorca, co pozwala uniknąć typów wartości boksu.
Dostawcy będący członkami unii
Typ unii może delegować członków unii do zagnieżdżonego IUnionMembers interfejsu. Gdy ten interfejs jest obecny, kompilator szuka Create metod fabrycznych zamiast konstruktorów:
[System.Runtime.CompilerServices.Union]
public record class Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
}
object? IUnionMembers.Value => _value;
}
Dostawcy składowych unii są przydatni, gdy typ unii wymaga konstruktora prywatnego lub gdy logika tworzenia wymaga wzorca fabryki, takiego jak typy record class unii.
Typy unii opartej na klasach
Klasa może być również typem unii. Ten typ unii jest przydatny, gdy potrzebujesz semantyki odwołań lub dziedziczenia:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
W przypadku związków opartych na klasach null wzorzec pasuje zarówno do odwołania o wartości null, jak i wartości null Value.
Implementacja unii
Typy unii opierają się na typach UnionAttribute i IUnion w System.Runtime.CompilerServices przestrzeni nazw. Środowisko uruchomieniowe obejmuje te typy począwszy od .NET 11 (wersja zapoznawcza 5):
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
Deklaracje unii generowane przez kompilator implementują IUnionelement . Możesz sprawdzić dowolną wartość unii w czasie wykonywania przy użyciu polecenia IUnion:
if (value is IUnion { Value: null }) { /* the union's value is null */ }
Podczas deklarowania union typu kompilator generuje strukturę, która implementuje IUnionelement . Na przykład Pet deklaracja (public union Pet(Cat, Dog, Bird);) staje się równoważna:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public Pet(Bird value) => Value = value;
public object? Value { get; }
}
specyfikacja języka C#
Aby uzyskać więcej informacji, zobacz specyfikację funkcji Unii .