Udostępnij za pośrednictwem


Typy unii (odwołanie w C#)

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 class lub struct, 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?"

Ważna

W programie .NET 11 (wersja zapoznawcza 2) środowisko uruchomieniowe nie zawiera interfejsu UnionAttribute i IUnion . Aby użyć typów unii, należy je zadeklarować samodzielnie. Aby wyświetlić wymagane deklaracje, zobacz Implementacja unii.

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 elementy członkowskie tworzenia unii:

public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        T single => [single],
        IEnumerable<T> multiple => multiple,
        _ => []
    };
}

Konwersje unii

Niejawna konwersja unii istnieje z każdego typu przypadku do typu unii. Nie musisz jawnie wywoływać konstruktora:

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. Aby uzyskać szczegółowe informacje na temat priorytetu konwersji, zobacz specyfikację języka.

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 unii

Gdy wzorzec jest zgodny z typem unii, wzorce mają zastosowanie 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
}

Dwa wzorce są wyjątkami od tej reguły: var wzorzec i wzorzec odrzucenia _ 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. Następujący wzorzec sprawdza, czy element Pet? nie ma wartości null iValue nie ma wartości null:

GetPet() switch
{
    var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks 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. Aby dopasować dowolny typ, nie musisz dołączać wzorca odrzucania (_) ani var 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
}

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
}

Możliwość przypisania wartości null

Kompilator śledzi stan null właściwości unii Value za pomocą następujących reguł:

  • Podczas tworzenia wartości unii na podstawie typu przypadku (za pomocą konstruktora lub konwersji unii) Value pobiera stan null wartości przychodzącej.
  • Gdy wzorzec HasValue dostępu innego niż boxing lub TryGetValue(...) elementy członkowskie wysyłają zapytanie do zawartości unii, stan Value null staje się "nie null" w true gałęzi.

Niestandardowe typy związków

Kompilator konwertuje deklarację union na deklarację struct . Struktura jest oznaczona atrybutem [System.Runtime.CompilerServices.Union] 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?.

Jeśli potrzebujesz innego zachowania — takiego jak związek oparty na klasach, niestandardowa strategia magazynu, obsługa międzyoperacyjności lub jeśli chcesz dostosować istniejący typ — możesz utworzyć typ unii ręcznie.

Każda klasa lub struktura z atrybutem jest typem unii, jeśli jest zgodny ze wzorcem[Union] podstawowym unii. Podstawowy wzorzec unii wymaga:

  • Atrybut [Union] typu.
  • Co najmniej jeden publiczny konstruktor, z których każdy ma jedną wartość in lub parametr. Typ parametru każdego konstruktora definiuje typ przypadku.
  • Właściwość publiczna Value typu object? (lub object) z akcesorem get .

Wszyscy członkowie związku muszą być publiczni. 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.

Kompilator zakłada, że niestandardowe typy unii spełniają następujące reguły zachowania:

  • Dźwięk: Value zawsze zwraca null lub wartość jednego z typów przypadków — nigdy nie jest to wartość innego typu. W przypadku związków default struktur tworzy element Value o wartości null.
  • Stabilność: jeśli tworzysz wartość unii na podstawie typu sprawy, Value pasuje do tego typu przypadku (lub jest to null , czy dane wejściowe to null).
  • 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: HasValue elementy członkowskie i TryGetValue , jeśli są obecne, zachowują się równoważnie do bezpośredniego sprawdzania Value .

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ść HasValue typu bool , która zwraca true wartość , gdy Value nie nulljest .
  • TryGetValue Metoda dla każdego typu przypadku, który zwraca bool i dostarcza wartość za pośrednictwem parametruout.
[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.

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

Następujące atrybuty i interfejs obsługują typy unii w czasie kompilacji i czasie wykonywania:

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; }
}

Ważna

W programie .NET 11 (wersja zapoznawcza 2) te typy nie są uwzględniane w środowisku uruchomieniowym. Aby używać typów unii, należy je zadeklarować w projekcie. Zostaną one uwzględnione w przyszłej wersji zapoznawczej platformy .NET.

specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz specyfikację funkcji Unii .

Zobacz także