Union-typer (C#-referens)

En unionstyp representerar ett värde som kan vara en av flera skiftlägestyper. Unioner tillhandahåller implicita konverteringar från varje ärendetyp, fullständig mönstermatchning och förbättrad nullability-spårning. Använd nyckelordet union för att deklarera en unionstyp:

public union Pet(Cat, Dog, Bird);

Den här deklarationen skapar en Pet union med tre falltyper: Cat, Dogoch Bird. Du kan tilldela valfritt skiftlägestypvärde till en Pet variabel. Kompilatorn ser till att switch uttrycken täcker alla skiftlägestyper.

C#-språkreferensen dokumenterar den senaste versionen av C#-språket. Den innehåller även inledande dokumentation för funktioner i offentliga förhandsversioner för den kommande språkversionen.

Dokumentationen identifierar alla funktioner som först introducerades i de tre senaste versionerna av språket eller i aktuella offentliga förhandsversioner.

Tips/Råd

Information om när en funktion först introducerades i C# finns i artikeln om språkversionshistoriken för C#.

Deklarera en union när ett värde måste vara exakt en av en fast uppsättning typer och du vill att kompilatorn ska framtvinga att alla möjligheter hanteras. Vanliga scenarier är:

  • Resultat-eller-fel returnerar: En metod returnerar antingen ett lyckat värde eller ett felvärde och anroparen måste hantera båda. En union som union Result(Success, Error) gör uppsättningen resultat explicita.
  • Meddelande- eller kommandosändning: Ett system bearbetar en sluten uppsättning meddelandetyper. En union säkerställer att nya meddelandetyper skapar kompileringstidsvarningar vid varje switch som inte hanterar dem ännu.
  • Ersätta markörgränssnitt eller abstrakta basklasser: Om du använder ett gränssnitt eller en abstrakt klass enbart för att gruppera typer för mönstermatchning ger en union fullständig kontroll utan att kräva arv eller delade medlemmar.

En union skiljer sig från andra typer av deklarationer på viktiga sätt:

  • Till skillnad från en class eller structdefinierar inte en union nya datamedlemmar. I stället består befintliga typer i en sluten uppsättning alternativ.
  • Till skillnad från en interface, stängs en union – du definierar den fullständiga listan över ärendetyper i deklarationen, och kompilatorn använder den listan för fullständighetskontroller.
  • Till skillnad från en recordlägger en union inte till likhets-, klonings- eller dekonstruktionsbeteende. En union fokuserar på "vilket fall är det?" snarare än "vilka fält har den?"

Union-typer förlitar sig på typerna UnionAttribute och IUnion i System.Runtime.CompilerServices namnområdet. .NET 11-körningen innehåller dessa typer.

Unionsdeklarationer

En unionsdeklaration anger ett namn och en lista över skiftlägestyper:

public union Pet(Cat, Dog, Bird);

Skiftlägestyper kan vara valfri typ som konverteras till object, inklusive klasser, structs, gränssnitt, typparametrar, nullbara typer och andra fackföreningar. I följande exempel visas olika möjligheter för skiftlägestyper:

public record class Cat(string Name);
public record class Dog(string Name, string Breed);
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);

När en ärendetyp är en värdetyp (t.ex int. ) rutor en unionsdeklaration värdet och lagrar det som en enda object? referens. Anpassade unionstyper kan använda en annan lagringsstrategi.

En facklig förklaring kan innehålla ett organ med ytterligare medlemmar, precis som en struct, som omfattas av vissa begränsningar. Unionsdeklarationer kan inte innehålla instansfält, automatiska egenskaper eller fältliknande händelser. Du kan inte heller deklarera offentliga konstruktorer med en enda parameter, eftersom kompilatorn genererar dessa konstruktorer som medlemmar i skapande av unionen. Alla konstruktorer som du deklarerar måste använda en this(...) initierare för att direkt eller indirekt delegera till en genererad konstruktor. Följande Length union lägger till en TotalMeters egenskap som använder mönstermatchning för att hantera varje skiftlägestyp, tillsammans med en Add metod som kombinerar två längder:

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

Unionskonverteringar

Det finns en implicit unionskonvertering från varje ärendetyp till unionstypen:

static void BasicConversion()
{
    Pet pet = new Dog("Rex", "Labrador");
    Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }

    Pet pet2 = new Cat("Whiskers");
    Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}

Union-konverteringar fungerar genom att anropa motsvarande genererade konstruktor. Om det finns en användardefinierad implicit konverteringsoperator för samma typ prioriteras den användardefinierade operatorn framför unionskonverteringen. Om mer än en falltyp är lika tillämplig för källvärdet är unionskonverteringen tvetydig och kompilatorn rapporterar ett fel. Mer information om konverteringsprioritet finns i funktionsspecifikationen.

En unionskonvertering till en nullbar unionstruct (T?) fungerar också när T är en unionstyp:

static void NullableUnionExample()
{
    Pet? maybePet = new Dog("Buddy", "Beagle");
    Pet? noPet = null;

    Console.WriteLine(Describe(maybePet)); // output: 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",
    };
}

Matchning av unionsmönster

När du använder ett typ- eller deklarationsmönster i ett unionsuttryck testar mönstret fackets innehåll, inte unionsinstansen. Ett mönster testar till exempel Dog om Value är en Dog:

static void PatternMatching()
{
    Pet pet = new Dog("Rex", "Labrador");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

Mönstrets formulär avgör vilket värde det testar:

Pattern Beteende för ett unionsuttryck
Typ- eller deklarationsmönster, till exempel Dog eller Dog dog Testar innehållet mot den angivna typen.
Egenskap eller positionsmönster med en typ, till exempel Dog { Breed: "Labrador" } Testar innehållet mot typen och tillämpar sedan de kapslade mönstren på det skiftlägesvärdet.
Egenskap eller positionsmönster utan typ, till exempel { Name: "Rex" } Använder unionsinstansens egenskaper eller Deconstruct metod.
Listmönster Använder den fackliga instansens antal och indexeringsmedlemmar, inte medlemmar i dess innehåll.
var eller ignorera _ mönster Samlar in eller matchar den inkommande unionsinstansen utan att packa upp den.
Konstant eller relationsmönster Testar innehållet. Mönstret null hanterar även en null-unionsreferens eller null-omslutning.

Definiera till exempel en Pet union med en skrivskyddad Name egenskap som delegerar till den inneslutna Cat eller Dog. Typmönstren i egenskapens switch uttryck testar innehållet:

public union Pet(Cat, Dog)
{
    public string Name => this switch
    {
        Cat cat => cat.Name,
        Dog dog => dog.Name,
        null => "",
    };
}

Det inskrivna mönstret Dog { Breed: "Labrador" } testar innehållet och läser hundens Breed, en egenskap som facket inte har. Det otypade mönstret { Name: "Rex" } läser fackets Name egenskap istället, så det matchar också en katt med namnet Rex:

Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" });           // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" });           // output: True

Mönstret not negerar sitt kapslade mönster. I pet is not nullkontrollerar det kapslade null mönstret innehållet, så not null är ett användbart has-a-value-test. Däremot kontrollerar ett tomt otypat egenskapsmönster, { }, unionsinstansen. Den matchar alltid en icke-inullerbar struct-union, även om Value den är null:

Pet empty = default;
Console.WriteLine(empty is { });       // output: True
Console.WriteLine(empty is not null);  // output: False
if (empty is var captured)
{
    Console.WriteLine(captured.Value is null); // output: True
}

Logiska mönster bevarar dessa skillnader. var pet I var pet and not nullsamlar in den inkommande unionen och not null testar om den har innehåll som inte är null. var I not null and var petavbildar mönstret fortfarande den inkommande unionen: not ändrar inte värdet som skickas till det högra mönstret. I skickar däremot Dog and { Breed: "Labrador" }typmönstret det matchade till egenskapsmönstret Dog .

Anmärkning

Ett typmönster måste vara kompatibelt med minst en skiftlägestyp. För deklarationerna i den Pet här artikeln pet is Pet genererar kompilatorfel cs8121: ingen av deras falltyper kan matcha Pet. Det är inte ett körningstest av unionsinstansen. Om du vill söka efter innehåll, använd pet is not null eller pet.Value is not null; för att avbilda själva unionen använder du ett var mönster.

Kort och kort, typ- och deklarationsmönster testar innehållet. Rekursiva mönster med en typ testar också innehållet. rekursiva mönster utan typ använder förbundets egna medlemmar. Dessa regler är desamma i ett is uttryck och ett switch uttryck.

Null-matchning

För struct-fackföreningar null kontrollerar mönstret om Value är null:

static void NullHandling()
{
    Pet pet = default;
    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
    Console.WriteLine(pet.Value is null); // output: True
}

För klassbaserade fackföreningar null lyckas när antingen själva unionsreferensen är null eller dess Value egenskap är 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 */ }

För nullable union struct types (Pet?) null lyckas när den nullbara omslutningen inte har något värde eller när den underliggande unionen är Value null.

Unionens fullständighet

Ett switch uttryck är uttömmande över en unions skiftlägestyper när det hanterar varje ärendetyp. Om nullability-analysen även tar hänsyn till innehållet som inte är null behöver du inte ta bort (_) eller var arm:

static void PatternMatching()
{
    Pet pet = new Dog("Rex", "Labrador");

    var name = pet switch
    {
        Dog d => d.Name,
        Cat c => c.Name,
        Bird b => b.Name,
    };
    Console.WriteLine(name); // output: Rex
}

Om du vill hantera null-innehåll föredrar du en null arm framför en var eller ignorera (_) arm. Mönstret null anger din avsikt och bevarar varningar när du lägger till en ny skiftlägestyp utan matchande arm. En var eller ignorera arm accepterar också alla nya fall, så kompilatorn kan inte varna dig om det saknade fallet.

När det spårade null-tillståndet Value är "kanske null" varnar kompilatorn också om du utelämnar en null arm. Till exempel NullablePet har en nullbar skiftlägestyp, så dess parameterinnehåll kan vara null:

public union NullablePet(Cat?, Dog, Bird);

static string DescribeNullableCase(NullablePet pet) => pet switch
{
    Cat cat => cat.Name,
    Dog dog => dog.Name,
    Bird bird => bird.Name,
    null => "no pet",
};

Om du null tar bort armen från den här växeln genereras varnings-CS8655.

Nullbar analys garanterar inte att en genererad union har icke-null-innehåll vid körning. En standardunion innehåller null, inklusive när det är ett standardinitierat fält eller matriselement. Om alla skiftlägestyper inte är ogiltiga är standardtillståndet Value för spårat ändå "inte null", så kompilatorn varnar inte nödvändigtvis för en utelämnad null arm. Hantera null för korrekthet när standardinnehållet kan nå din växel:

static void NullHandling()
{
    Pet pet = default;
    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
    Console.WriteLine(pet.Value is null); // output: True
}

I det här Pet exemplet genererar borttagning av null armen ingen varning om att null-arm saknas om inte tidigare kod ändrar det spårade null-tillståndet, men växeln genererar SwitchExpressionException när den tar emot standardvärdet. Ett tidigare is null test kan ändra det spårade tillståndet och orsaka en varning. Det här beteendet är separat från en bestämd tilldelning: kompilatorn avvisar en läsning av en otilldelad lokal variabel.

Nullbarhet

Kompilatorn spårar null-tillståndet för en unions Value egenskap genom följande regler:

  • Standardvärdet null för en unions Value egenskap är "kanske null" om standardvärdet null för alla falltyper är "kanske null". Annars är standardvärdet null "inte null".
  • När du skapar ett union-värde från en ärendetyp (via en konstruktor eller unionkonvertering) Value får du null-tillståndet för det inkommande värdet.
  • När icke-boxningsåtkomstmönstrets HasValue eller TryGetValue(...) medlemmarna frågar fackets innehåll blir null-tillståndet Value för "inte null" på grenen true .

Anpassade unionstyper

Kompilatorn konverterar en union deklaration till en struct deklaration. Structen är markerad med [System.Runtime.CompilerServices.Union] attributet och implementerar IUnion gränssnittet. Den innehåller en offentlig konstruktor för varje ärendetyp och en Value egenskap. Språket tillhandahåller implicita unionskonverteringar som anropar dessa konstruktorer. Det genererade formuläret är åsiktsutlåtande. Det är alltid en struct, rutor alltid värdetypsfall och lagrar alltid innehåll som object?.

Du kan behöva ett annat beteende om du vill anpassa en befintlig typ, skapa en klassbaserad union eller använda en anpassad lagringsstrategi eller om du behöver interop-stöd. Du kan skapa en unionstyp manuellt.

En klass eller struct med ett [Union] attribut är en unionstyp om den följer det grundläggande unionsmönstret. Det grundläggande unionsmönstret kräver:

  • Ett [Union] attribut för typen.
  • En eller flera offentliga konstruktorer, var och en med ett enda värde eller in en parameter. Parametertypen för varje konstruktor definierar en skiftlägestyp.
  • En offentlig Value egenskap av typen object? (eller object) med en get accessor.

Alla föregående fackföreningsmedlemmar måste vara offentliga. Kompilatorn använder dessa medlemmar för att implementera fackliga konverteringar, mönstermatchning och fullständighetskontroller. Du kan också implementera åtkomstmönstret för icke-boxning eller skapa en klassbaserad unionstyp. Din anpassade unionstyp kan lägga till fler medlemmar.

Kompilatorn förutsätter att anpassade unionstyper uppfyller dessa beteenderegler:

  • Ljud:Value returnerar null alltid eller ett värde för en av skiftlägestyperna, inklusive för ett standardvärde för struct. En anpassad union kan representera standardvärdet som ett icke-null-ärende, till skillnad från nullinnehållet i en standardgenererad union.
  • Stabilitet: Om du skapar ett union-värde från en ärendetyp Value matchar den ärendetypen (eller om null indata var null).
  • Ekvivalens för skapande: Om ett värde implicit kan konverteras till två olika skiftlägestyper skapar båda medlemmarna samma observerbara beteende.
  • Konsekvens för åtkomstmönster: Medlemmarna HasValue och TryGetValue , om de finns, beter sig på samma sätt som kontroll Value direkt.

I följande exempel visas en anpassad unionstyp:

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

Åtkomstmönster som inte är boxning

En anpassad unionstyp kan om du vill implementera åtkomstmönstret för icke-boxning för att ge starkt skrivskyddad åtkomst till värdetypsfall utan boxning under mönstermatchning. Det här mönstret kräver:

  • En HasValue egenskap av typen bool som returnerar true när Value inte nullär .
  • En TryGetValue metod för varje skiftlägestyp som returnerar bool och levererar värdet via en out parameter. TryGetValue returnerar true endast när Value är ett icke-null-värde av den typen. Parameterns out typ är identitetskonverterad till skiftlägestypen eller till den underliggande värdetypen när skiftlägestypen är en nullbar värdetyp.
[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
}

För mönstermatchning anropar TryGetValue kompilatorn typmönsterkontroller (till exempel union is T) och HasValue för null-mönsterkontroller (till exempel union is null), för att undvika boxning för värdetypsfall. Varje medlem gäller för sin egen mönstertyp – inte heller en reserv för den andra. När en medlem saknas kontrollerar Value kompilatorn egenskapen efter det mönstret i stället. Mer information finns i Hur kompilatorn genererar kod för mönstermatchning.

Unionsmedlemsleverantörer

En fackföreningstyp kan delegera sina fackföreningsmedlemmar till ett offentligt kapslat IUnionMembers gränssnitt som den direkt innehåller och implementerar. Det gränssnittet är den fackliga medlemsprovidern. Kompilatorn hittar endast fackföreningsmedlemmar i providergränssnittet, inklusive ärvda medlemmar. Medlemmar som deklareras endast för själva fackföreningstypen används inte som fackföreningsmedlemmar.

När du använder en unionsmedlemsprovider måste gränssnittet deklarera följande obligatoriska medlemmar:

  • Statiska Create metoder: En för varje ärendetyp, med en enskild parameter och en returtyp som kan konverteras till union-typen. Dessa metoder etablerar dina ärendetyper.
  • Value egenskap: En offentlig object egenskap eller object? egenskap med en get accessor som returnerar det inneslutna värdet.

Du kan också deklarera dessa medlemmar i gränssnittet så att kompilatorn genererar effektivare mönstermatchningskod:

  • TryGetValue metoder: En för varje skiftlägestyp som returneras bool med en out parameter av skiftlägestypen.
  • HasValue egenskap: En offentlig bool egenskap med en get accessor som returneras true när Value inte nullär .

Hur kompilatorn genererar kod för mönstermatchning: TryGetValue och HasValue gäller för olika typer av mönster – inte heller en återställning för den andra:

  • För ett typmönster, till exempel union is T, anropar TryGetValue(out T value) kompilatorn när det deklareras, extraherar värdet och tillämpar mönstret på det utan boxning. I annat fall tillämpar kompilatorn mönstret på Value egenskapen, som utför en körningstypskontroll mot T och kan markera värdet när T är en värdetyp.
  • För ett null-mönster, till exempel union is null, anropar HasValue kompilatorn när den deklareras för att testa om unionen innehåller ett värde. Annars tillämpar kompilatorn null-mönstret direkt på Value egenskapen.

Varje medlem är oberoende valfritt: utan TryGetValueanvänder typmönster Value; utan HasValueanvänder null-mönster Value. Om du deklarerar någon av medlemarna kan kompilatorn generera effektivare, starkt skriven kod för den mönstertypen:

[System.Runtime.CompilerServices.Union]
public struct 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; }

        // Optional but recommended: TryGetValue enables efficient pattern matching
        bool TryGetValue(out T value);
        bool TryGetValue(out Exception value);
    }

    object? IUnionMembers.Value => _value;

    public bool TryGetValue(out T value)
    {
        if (_value is T t)
        {
            value = t;
            return true;
        }
        value = default!;
        return false;
    }

    public bool TryGetValue(out Exception value)
    {
        if (_value is Exception e)
        {
            value = e;
            return true;
        }
        value = default!;
        return false;
    }
}

Unionsmedlemsleverantörer är användbara när unionstypen behöver en privat konstruktor eller när skapandelogik kräver ett fabriksmönster, till exempel med record class unionstyper.

Klassbaserade unionstyper

En klass kan också vara en unionstyp. Den här typen av union är användbar när du behöver referenssemantik eller arv:

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

För klassbaserade unioner null matchar mönstret både en null-referens och en null Value.

Språkspecifikation för C#

Mer information finns i Unions funktionsspecifikation .

Se även