Egyesítő típusok (C#-referencia)

Az egyesítő típus olyan értéket jelöl, amely több esettípus egyike lehet. Az egyes esettípusok implicit konverzióit, a teljes mintaegyezést és a továbbfejlesztett nullability trackingt biztosítják. union A kulcsszóval deklarálhat egy egyesítési típust:

public union Pet(Cat, Dog, Bird);

Ez a deklaráció három esettípussal hoz létre egy Pet egyesítést: Cat, Dogés Bird. Bármilyen esettípus-értéket hozzárendelhet egy Pet változóhoz. A fordító biztosítja, hogy a switch kifejezések minden esettípusra kiterjednek.

A C# nyelv referenciadokumentuma a C# nyelv legújabb kiadású verzióját ismerteti. Emellett a közelgő nyelvi kiadás nyilvános előzetes verziójú funkcióinak kezdeti dokumentációját is tartalmazza.

A dokumentáció azonosítja azokat a funkciókat, amelyeket először a nyelv utolsó három verziójában vagy az aktuális nyilvános előzetes verziókban vezetnek be.

Jótanács

Ha meg szeretné tudni, hogy mikor jelent meg először egy funkció a C#-ban, tekintse meg a C# nyelvi verzióelőzményeiről szóló cikket.

Deklaráljon egy egyesítést, ha egy értéknek pontosan egy rögzített típuskészletnek kell lennie, és azt szeretné, hogy a fordító kényszerítse minden lehetőség kezelését. Gyakori forgatókönyvek a következők:

  • Eredmény vagy hiba: A metódus sikerértéket vagy hibaértéket ad vissza, a hívónak mindkettőt kezelnie kell. Egy ilyen union Result(Success, Error) egyesítés explicitvé teszi az eredmények halmazát.
  • Üzenet- vagy parancsküldés: A rendszer egy zárt üzenettípus-készletet dolgoz fel. Az egyesítők biztosítják, hogy az új üzenettípusok fordítási idejű figyelmeztetéseket készítsenek minden switch olyan üzenetnél, amely még nem kezeli őket.
  • Jelölőillesztők vagy absztrakt alaposztályok cseréje: Ha egy felületi vagy absztrakt osztályt kizárólag a mintaegyeztetés típusainak csoportosítására használ, a egyesítés teljes körű ellenőrzést biztosít öröklés vagy megosztott tagok megkövetelése nélkül.

Az egyesítők fontos módon különböznek a többi típusdeklarációtól:

  • Egy vagy classtöbb struct egyesítéssel ellentétben az egyesítők nem határoznak meg új adattagokat. Ehelyett a meglévő típusokat egy zárt alternatívahalmazba állítja össze.
  • interfaceA egyesítéstől eltérően a deklarációban meg kell határoznia az esettípusok teljes listáját, és a fordító ezt a listát használja a teljesség ellenőrzéséhez.
  • recordA egyesítéssel ellentétben a egyesítés nem ad egyenlőséget, klónozást vagy dekonstruálási viselkedést. Az egyesítők inkább a "melyik esetet" helyezik előtérbe, nem pedig a "milyen mezőkkel rendelkeznek?"

Uniós nyilatkozatok

Az egyesítő deklarációk az esettípusok nevét és listáját határozzák meg:

public union Pet(Cat, Dog, Bird);

Az esettípusok bármilyen típusúak lehetnek, amelyek osztályokat object, szerkezeteket, interfészeket, típusparamétereket, null értékű típusokat és más egyesítőket is átalakítanak. Az alábbi példák különböző esettípus-lehetőségeket mutatnak be:

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

Ha egy esettípus értéktípus (például int), akkor az érték akkor lesz bekeretítve, ha az egyesítő Value tulajdonságában van tárolva. A szakszervezetek egyetlen object? hivatkozásként tárolják a tartalmukat.

Az egyesítő nyilatkozatok tartalmazhatnak olyan testületet is, amely további tagokkal rendelkezik, csakúgy, mint egy struktúra, bizonyos korlátozások mellett. Az egyesítő deklarációk nem tartalmazhatnak példánymezőket, automatikus tulajdonságokat vagy mezőszerű eseményeket. A nyilvános konstruktorok egyetlen paraméterrel sem deklarálhatók, mert a fordító ezeket a konstruktorokat egyesítő létrehozási tagokként hozza létre. Az alábbi Length összevonás egy olyan tulajdonságot TotalMeters ad hozzá, amely mintaegyeztetéssel kezeli az összes esettípust, valamint egy Add olyan metódust, amely két hosszt egyesít:

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

Uniós átalakítások

Az implicit egyesítő átalakítás minden esettípustól az egyesítő típusig létezik:

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

Az egyesítő átalakítások a megfelelő generált konstruktor meghívásával működnek. Ha egy felhasználó által definiált implicit konverziós operátor létezik ugyanahhoz a típushoz, a felhasználó által definiált operátor elsőbbséget élvez az egyesítő átalakítással szemben. Ha egynél több esettípus egyaránt alkalmazható a forrásértékre, az egyesítő átalakítás nem egyértelmű, és a fordító hibát jelez. A konverziós prioritással kapcsolatos részletekért tekintse meg a szolgáltatás specifikációját.

A null értékű egyesítési szerkezetre (T?) való egyesítés akkor is működik, ha T egy egyesítési típus:

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

Egyesítő minta egyeztetése

Ha a minta egyezik az egyesítő típussal, a minták általában az unió tulajdonságára Value vonatkoznak, nem magára az egyesítő értékre. Ez a "feloldás" viselkedés azt jelenti, hogy az egyesítés transzparens a mintaegyezéshez:

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
}

A szabály alól három minta kivételt képez: az elvetési _ minta, a var minta és a not minta az egyesítő értékre vonatkozik, nem pedig a tulajdonságára Value . Az egyesítő érték varGetPet() rögzítésére használható Pet? a (Nullable<Pet>):

if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }

A logikai mintákban minden ág egyenként követi a kibontási szabályt. A minta bal ága módosíthatja and a jobb ág által látott bejövő értéket. Mivel a minta a not bejövő egyesítő értékre vonatkozik ahelyett, hogy annak Value, a bevezető not null nem bontja ki az azt követő ág értékét:

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 => ...,
}

Megjegyzés:

Mivel a minták vonatkoznak rá Value, az olyan minták, mint pet is Pet általában nem egyeznek, mivel Pet az unió tartalmával van tesztelve, és nem magát az uniót.

Null egyezés

A struct unions esetében a null minta ellenőrzi, hogy null-e Value :

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
}

Osztályalapú egyesítések esetén akkor sikeres, null ha az egyesítő hivatkozás maga null, vagy a tulajdonsága Value null:

Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }

Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }

A null értékű egyesítőszerkezet-típusok (Pet?) null akkor sikeresek, ha a null értékű burkolónak nincs értéke, vagy ha a mögöttes egyesítés Value null értékű.

Az unió teljessége

A switch kifejezések teljes körűek, ha az összes egyesítő esettípust kezeli. A fordító csak akkor figyelmeztet, ha a kis- és nagybetűket nem kezeli. Nem kell olyan elvetési mintát (_) vagy var mintát felvennie, amely bármilyen típusnak megfelel, ha a kifejezés egyértelműen hozzárendelve van:

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
}

Ha az unió Value tulajdonságának null állapota "talán null", akkor a figyelmeztetés elkerülése érdekében a következőt kell kezelnie 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
}

Ez a helyzet akkor fordulhat elő, ha a union kifejezés az alapértelmezett érték, vagy nincs egyértelműen hozzárendelve, ahogy az előző példában is látható.

Null lehetőség

A fordító az alábbi szabályokkal követi nyomon az unió tulajdonságának Value null állapotát:

  • Az egyesítő Value tulajdonság alapértelmezett null állapota "talán null", ha bármilyen esetben az alapértelmezett null állapot "talán null". Ellenkező esetben az alapértelmezett null állapot "nem null".
  • Ha egy esettípusból (konstruktoron vagy egyesítési átalakításon keresztül) hoz létre egyesítő értéket, Value a bejövő érték null állapotát kapja meg.
  • Amikor a nem boxing hozzáférési minta HasValue vagy TryGetValue(...) tagok lekérdezik az unió tartalmát, az ág null állapota Value "nem null" true lesz.

Egyéni egyesítő típusok

A fordító deklarációvá union alakítja a deklarációt struct . A szerkezet az attribútummal [System.Runtime.CompilerServices.Union] van megjelölve, és implementálja az interfészt IUnion . Tartalmaz egy nyilvános konstruktort és egy implicit konverziót minden esettípushoz egy Value tulajdonsággal együtt. A létrehozott űrlap véleményezhető. Ez mindig egy struktúra, mindig érték típusú eseteket ad meg, és a tartalmat object?mindig a következőképpen tárolja.

Eltérő viselkedésre lehet szükség, ha egy meglévő típust szeretne átalakítani, osztályalapú uniót szeretne létrehozni, vagy egyéni tárolási stratégiát szeretne használni, vagy ha interop-támogatásra van szüksége. Az egyesítő típusokat manuálisan is létrehozhatja.

Bármely attribútummal rendelkező [Union] osztály vagy struktúra egyesítő típus , ha az alapszintű egyesítési mintát követi. Az alapszintű egyesítő minta a következőt igényli:

  • Egy [Union] attribútum a típuson.
  • Egy vagy több nyilvános konstruktor, amelyek mindegyike egyetlen értékkel vagy in paraméterrel rendelkezik. Az egyes konstruktorok paramétertípusa egy esettípust határoz meg.
  • Olyan köztulajdon Value , amelynek típusa object? (vagy object) tartozékkal van eltulajdonítva get .

Az előző szakszervezeti tagoknak nyilvánosnak kell lenniük. A fordító ezeket a tagokat használja az egyesítő átalakítások, a minták egyeztetése és a teljesség ellenőrzése érdekében. Implementálhatja a nem boxing hozzáférési mintát is, vagy létrehozhat egy osztályalapú uniótípust. Az egyéni egyesítő típus további tagokat adhat hozzá.

A fordító feltételezi, hogy az egyéni egyesítő típusok megfelelnek az alábbi viselkedési szabályoknak:

  • Hang:Value mindig az egyik esettípus értékét adja vissza null – soha ne adjon meg más típusú értéket. A struct unions esetében default a Valuenull.
  • Stabilitás: Ha egyesítő értéket hoz létre egy kis- és nagybetűtípusból, Value akkor az adott esettípusnak megfelelő (vagy null ha a bemenet volt null).
  • Létrehozási egyenértékűség: Ha egy érték implicit módon két különböző esettípusra konvertálható, mindkét létrehozási tag ugyanazt a megfigyelhető viselkedést hozza létre.
  • Hozzáférési minta konzisztenciája: A HasValueTryGetValue tagok és ha vannak, ugyanúgy viselkednek, mint a közvetlen ellenőrzés Value .

Az alábbi példa egy egyéni egyesítő típust mutat be:

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

Nem boxing hozzáférési minta

Az egyéni egyesítési típusok opcionálisan implementálhatják a nem dobozoló hozzáférési mintát , hogy az érték típusú esetekhez való erős hozzáférést lehetővé tegyék anélkül, hogy a mintaegyezés során boxolást hajtanak végre. Ehhez a mintához a következők szükségesek:

  • Olyan HasValue típusú bool tulajdonság, amely akkor ad true vissza, ha Value nem null.
  • Egy TryGetValue metódus minden egyes esettípushoz, amely egy paraméteren keresztül bool adja vissza out és kézbesíti az értéket. TryGetValue csak akkor ad true vissza, ha Value az adott esettípus nem null értékű. A out paraméter típusa identitás-átalakítható a kis- és nagybetűk típusára, illetve az alapul szolgáló értéktípusra, ha a kis- és nagybetűk értéke null értékű.
[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
}

A fordító előnyben részesíti a tulajdonságot TryGetValue a Value mintaegyeztetés megvalósításakor, ami elkerüli az értéktípusok bevitelét.

Uniós tagszolgáltatók

Az egyesítő típusok egymásba ágyazott felületre delegálhatják a szakszervezet tagjait IUnionMembers . Ha ez a felület jelen van, a fordító a konstruktorok helyett gyári metódusokat keres Create :

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

Az uniótag-szolgáltatók akkor hasznosak, ha az egyesítő típushoz magánkonstruktorra van szükség, vagy ha a létrehozási logikához gyári minta szükséges, például egyesítő típusok esetén record class .

Osztályalapú egyesítő típusok

Az osztály lehet egyesítő típus is. Az ilyen típusú egyesítés akkor hasznos, ha referencia szemantikára vagy öröklésre van szüksége:

[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",
    };
}

Az osztályalapú egyesítések esetében a null minta egy null hivatkozással és null értékkel Valueis megegyezik.

Uniós végrehajtás

Az egyesítő típusok a UnionAttribute névtérben lévő és IUnion a System.Runtime.CompilerServices típusokra támaszkodnak. A futtatókörnyezet az alábbi típusokat tartalmazza a .NET 11 5. előzetes verziótól kezdve:

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;

public interface IUnion
{
    object? Value { get; }
}

A fordító IUnionáltal létrehozott uniós deklarációk. Az egyesítő értékeket futásidőben a következővel IUnionellenőrizheti:

if (value is IUnion { Value: null }) { /* the union's value is null */ }

Amikor deklarál egy típust union , a fordító létrehoz egy szerkezetet, amely megvalósítja azokat IUnion. A deklaráció (Pet) például public union Pet(Cat, Dog, Bird); a következőnek felel meg:

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

C# nyelvspecifikáció

További információ: Unions feature specification.

Lásd még