Dekonstruieren von Tupeln und anderen Typen

Ein Tupel stellt einen einfachen Weg bereit, um mehrere Werte aus einem Methodenaufruf abzurufen. Sobald Sie den Tupel abrufen, müssen Sie jedoch seine individuellen Elemente bearbeiten. Jedes Element einzeln zu bearbeiten ist jedoch mühselig, wie das folgende Beispiel zeigt. Die Methode QueryCityData gibt ein Dreiertupel zurück, und jedes seiner Elemente wird in einem separaten Vorgang einer Variablen zugewiesen.

public class Example
{
    public static void Main()
    {
        var result = QueryCityData("New York City");

        var city = result.Item1;
        var pop = result.Item2;
        var size = result.Item3;

         // Do something with the data.
    }

    private static (string, int, double) QueryCityData(string name)
    {
        if (name == "New York City")
            return (name, 8175133, 468.48);

        return ("", 0, 0);
    }
}

Das Abrufen mehrerer Feld- und Eigenschaftswerte aus einem Objekt kann ebenso mühsam sein: Sie müssen einen Feld- oder Eigenschaftswert einer Variablen auf Member-für-Member-Basis zuweisen.

Sie können in einem einzigen deconstruct-Vorgang mehrere Elemente aus einem Tupel oder mehrere berechnete, Feld- und Eigenschaftswerte aus einem Objekt abrufen. Um ein Tupel zu dekonstruieren, weisen Sie seine Elemente einzelnen Variablen zu. Wenn Sie ein Objekt dekonstruieren, weisen Sie bestimmte Elemente einzelnen Variablen zu.

Tupel

Die Features von C# bieten eine integrierte Unterstützung für Dekonstruieren von Tupeln, sodass Sie alle Elemente in einem Tupel mit einem einzigen Vorgang entpacken können. Die allgemeine Syntax für das Dekonstruieren eines Tupel ist ähnlich der Syntax für das Definieren eines Tupel: Auf der linken Seite einer Zuweisungsanweisung umschließen Sie die Variablen, denen die Elemente zugewiesen werden sollen, mit Klammern. Die folgende Anweisung weist die Elemente eines Vierertupels beispielsweise vier einzelnen Variablen zu:

var (name, address, city, zip) = contact.GetAddressInfo();

Es gibt drei Wege zum Dekonstruieren eines Tupels:

  • Sie können den Typ jedes Felds innerhalb der Klammern explizit deklarieren. Das folgende Beispiel verwendet diese Methode, um das Dreiertupel zu dekonstruieren, das von der QueryCityData-Methode zurückgegeben wurde.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Sie können das Schlüsselwort var verwenden, damit C# den Typ jeder Variable herleitet. Platzieren Sie das Schlüsselwort var außerhalb der Klammern. Im folgenden Beispiel wird ein Typrückschluss beim Dekonstruieren des von der QueryCityData-Methode zurückgegebenen Dreiertupels verwendet.

    public static void Main()
    {
        var (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    Sie können das Schlüsselwort var auch einzeln mit beliebigen oder allen Variablendeklarationen innerhalb der Klammern verwenden.

    public static void Main()
    {
        (string city, var population, var area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    Dies ist jedoch sehr mühselig und wird nicht empfohlen.

  • Schließlich können Sie das Tupel in bereits deklarierte Variablen dekonstruieren.

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
        double area = 144.8;
    
        (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Ab C# 10 können Sie die Variablendeklaration und -zuweisung in einer Dekonstruktion kombinieren.

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
    
        (city, population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

Sie können keinen bestimmten Typ außerhalb der Klammern angeben, auch wenn jedes Feld im Tupel den selben Typ hat. Dadurch wird der Compilerfehler CS8136 „Durch Dekonstruktion der Form „var (...)“ wird ein bestimmter Typ für „var“ unzulässig“ generiert.

Sie müssen ebenfalls jedes Element des Tupels einer Variablen zuweisen. Wenn Sie Elemente auslassen, generiert der Compiler den Fehler: CS8132, „Tupel mit ‚x‘ Elementen kann nicht in ‚y‘ Variablen dekonstruiert werden.“

Tupelelemente mit Ausschussvariablen

Häufig sind Sie beim Dekonstruieren eines Tupel nur an den Werten mancher Elemente interessiert. Sie können die Unterstützung von C# für Ausschussvariablen nutzen, d. h. für schreibgeschützte Variablen, deren Werte Sie ignorieren möchten. Eine Ausschussvariable wird in einer Zuweisung durch einen Unterstrich („_“) angegeben. Sie können beliebig viele Werte verwerfen, diese werden alle in einem einzigen Ausschuss dargestellt, _.

Das folgende Beispiel veranschaulicht die Verwendung von Tupels mit Ausschüssen. Die QueryCityDataForYears-Methode gibt ein Sechsertupel mit dem Namen einer Stadt, ihrer Fläche, einer Jahreszahl, der Bevölkerung der Stadt in diesem Jahr, einer zweiten Jahreszahl und der Bevölkerung der Stadt im zweiten Jahr zurück. Das Beispiel zeigt die Veränderung der Bevölkerung zwischen diesen beiden Jahren. Von den Daten, die im Tupel verfügbar sind, ist die Fläche der Stadt nicht relevant für uns und außerdem kennen wir den Namen der Stadt und die zwei Datumswerte zur Entwurfszeit. Darum sind wir nur an den zwei Bevölkerungsgwerten interessiert, die im Tupel gespeichert sind und behandeln die restlichen Werte als Ausschuss.

using System;

public class ExampleDiscard
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Benutzerdefinierte Typen

C# bietet keine integrierte Unterstützung für die Dekonstruktion anderer Nicht-Tupeltypen als record und DictionaryEntry. Als Autor einer Klasse, Struktur oder Schnittstelle können Sie jedoch den Instanzen des Typs das Dekonstruieren durch die Implementierung von mindestens einer Methode Deconstruct gestatten. Die Methode gibt „void“ zurück und jeder Wert, der dekonstruiert werden soll, wird durch den Parameter out in der Methodensignatur angegeben. Die folgende Methode Deconstruct einer Person-Klasse gibt beispielsweise den Vor-, Zweit- und Nachnamen zurück:

public void Deconstruct(out string fname, out string mname, out string lname)

Sie können dann eine Instanz der Person-Klasse mit dem Namen p mit einer Zuweisung wie im folgenden Code dekonstruieren:

var (fName, mName, lName) = p;

Das folgende Beispiel überlädt die Methode Deconstruct, um verschiedene Kombinationen von Eigenschaften eines Person-Objekts zurückzugeben. Einzelne Überladungen geben Folgendes zurück:

  • Einen Vor- und Nachnamen.
  • Ein Vorname, ein zweiter Vorname und ein Nachname
  • Einen Vor- und Nachnamen, einen Namen einer Stadt und eines Staats.
using System;

public class Person
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string City { get; set; }
    public string State { get; set; }

    public Person(string fname, string mname, string lname,
                  string cityName, string stateName)
    {
        FirstName = fname;
        MiddleName = mname;
        LastName = lname;
        City = cityName;
        State = stateName;
    }

    // Return the first and last name.
    public void Deconstruct(out string fname, out string lname)
    {
        fname = FirstName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string mname, out string lname)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string lname,
                            out string city, out string state)
    {
        fname = FirstName;
        lname = LastName;
        city = City;
        state = State;
    }
}

public class ExampleClassDeconstruction
{
    public static void Main()
    {
        var p = new Person("John", "Quincy", "Adams", "Boston", "MA");

        // Deconstruct the person object.
        var (fName, lName, city, state) = p;
        Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
    }
}
// The example displays the following output:
//    Hello John Adams of Boston, MA!

Mehrere Deconstruct-Methoden mit der gleichen Anzahl von Parametern sind mehrdeutig. Sie müssen darauf achten, Deconstruct-Methoden mit einer unterschiedlichen Anzahl von Parametern oder „Stelligkeit“ zu definieren. Deconstruct-Methoden mit der gleichen Anzahl von Parametern können bei der Überladungsauflösung nicht unterschieden werden.

Benutzerdefinierter Typ mit Ausschussvariablen

Genau wie bei Tupels können Sie Ausschüsse verwenden, um ausgewählte Elemente zu ignorieren, die von einer Deconstruct-Methode zurückgegeben werden. Jeder Ausschuss wird von einer Variable mit dem Namen „_“ definiert, und ein einziger Dekonstruierungsvorgang kann mehrere Ausschüsse beinhalten.

Im folgenden Beispiel wird ein Person-Objekt in vier Zeichenfolgen (den Vor- und Nachnamen, die Stadt und den Staat) dekonstruiert, der Nachname und der Staat werden jedoch verworfen.

// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
//      Hello John of Boston!

Erweiterungsmethoden für benutzerdefinierte Typen

Wenn Sie nicht der Autor einer Klasse, Struktur oder Schnittstelle sind, können Sie die Objekte dieses Typs dennoch dekonstruieren, indem Sie eine oder mehrere Deconstruct extension methods (Erweiterungsmethoden) implementieren, um die Werte zurückzugeben, an denen Sie interessiert sind.

Im folgenden Beispiel werden zwei Deconstruct-Erweiterungsmethoden für die Klasse System.Reflection.PropertyInfo definiert. Die erste gibt einen Satz von Werten zurück, der die Merkmale der Eigenschaft angibt, einschließlich ihres Typs, ob es sich dabei um eine statische oder eine Instanzeigenschaft handelt und ob die Eigenschaft schreibgeschützt oder indiziert ist. Die zweite gibt die Zugriffsebene der Eigenschaft an. Da die Zugriffsebene von Get- und Set-Zugriffsmethoden Unterschiede aufweisen kann, geben boolesche Werte an, ob die Eigenschaft über separate Get- und Set-Zugriffsmethoden verfügt und, wenn dies der Fall ist, ob sie über dieselbe Zugriffsebene verfügen. Wenn es nur eine Zugriffsmethode gibt oder die Get- und Set-Zugriffsmethode über dieselbe Zugriffsebene verfügen, gibt die Variable access die Zugriffsebene der Eigenschaft als Ganzes an. Andernfalls wird die Zugriffsebene der Get- und Set-Zugriffsmethoden von den Variablen getAccess und setAccess angezeigt.

using System;
using System.Collections.Generic;
using System.Reflection;

public static class ReflectionExtensions
{
    public static void Deconstruct(this PropertyInfo p, out bool isStatic,
                                   out bool isReadOnly, out bool isIndexed,
                                   out Type propertyType)
    {
        var getter = p.GetMethod;

        // Is the property read-only?
        isReadOnly = ! p.CanWrite;

        // Is the property instance or static?
        isStatic = getter.IsStatic;

        // Is the property indexed?
        isIndexed = p.GetIndexParameters().Length > 0;

        // Get the property type.
        propertyType = p.PropertyType;
    }

    public static void Deconstruct(this PropertyInfo p, out bool hasGetAndSet,
                                   out bool sameAccess, out string access,
                                   out string getAccess, out string setAccess)
    {
        hasGetAndSet = sameAccess = false;
        string getAccessTemp = null;
        string setAccessTemp = null;

        MethodInfo getter = null;
        if (p.CanRead)
            getter = p.GetMethod;

        MethodInfo setter = null;
        if (p.CanWrite)
            setter = p.SetMethod;

        if (setter != null && getter != null)
            hasGetAndSet = true;

        if (getter != null)
        {
            if (getter.IsPublic)
                getAccessTemp = "public";
            else if (getter.IsPrivate)
                getAccessTemp = "private";
            else if (getter.IsAssembly)
                getAccessTemp = "internal";
            else if (getter.IsFamily)
                getAccessTemp = "protected";
            else if (getter.IsFamilyOrAssembly)
                getAccessTemp = "protected internal";
        }

        if (setter != null)
        {
            if (setter.IsPublic)
                setAccessTemp = "public";
            else if (setter.IsPrivate)
                setAccessTemp = "private";
            else if (setter.IsAssembly)
                setAccessTemp = "internal";
            else if (setter.IsFamily)
                setAccessTemp = "protected";
            else if (setter.IsFamilyOrAssembly)
                setAccessTemp = "protected internal";
        }

        // Are the accessibility of the getter and setter the same?
        if (setAccessTemp == getAccessTemp)
        {
            sameAccess = true;
            access = getAccessTemp;
            getAccess = setAccess = String.Empty;
        }
        else
        {
            access = null;
            getAccess = getAccessTemp;
            setAccess = setAccessTemp;
        }
    }
}

public class ExampleExtension
{
    public static void Main()
    {
        Type dateType = typeof(DateTime);
        PropertyInfo prop = dateType.GetProperty("Now");
        var (isStatic, isRO, isIndexed, propType) = prop;
        Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:");
        Console.WriteLine($"   PropertyType: {propType.Name}");
        Console.WriteLine($"   Static:       {isStatic}");
        Console.WriteLine($"   Read-only:    {isRO}");
        Console.WriteLine($"   Indexed:      {isIndexed}");

        Type listType = typeof(List<>);
        prop = listType.GetProperty("Item",
                                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
        var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop;
        Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: ");

        if (!hasGetAndSet | sameAccess)
        {
            Console.WriteLine(accessibility);
        }
        else
        {
            Console.WriteLine($"\n   The get accessor: {getAccessibility}");
            Console.WriteLine($"   The set accessor: {setAccessibility}");
        }
    }
}
// The example displays the following output:
//       The System.DateTime.Now property:
//          PropertyType: DateTime
//          Static:       True
//          Read-only:    True
//          Indexed:      False
//
//       Accessibility of the System.Collections.Generic.List`1.Item property: public

Erweiterungsmethode für Systemtypen

Einige Systemtypen stellen zur Vereinfachung die Deconstruct-Methode bereit. Beispielsweise stellt der Typ System.Collections.Generic.KeyValuePair<TKey,TValue> diese Funktionalität bereit. Wenn Sie über ein System.Collections.Generic.Dictionary<TKey,TValue> iterieren, ist jedes Element ein KeyValuePair<TKey, TValue> und kann dekonstruiert werden. Betrachten Sie das folgenden Beispiel:

Dictionary<string, int> snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase)
{
    ["https://github.com/dotnet/docs"] = 16_465,
    ["https://github.com/dotnet/runtime"] = 114_223,
    ["https://github.com/dotnet/installer"] = 22_436,
    ["https://github.com/dotnet/roslyn"] = 79_484,
    ["https://github.com/dotnet/aspnetcore"] = 48_386
};

foreach (var (repo, commitCount) in snapshotCommitMap)
{
    Console.WriteLine(
        $"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021.");
}

Sie können eine Deconstruct-Methode Systemtypen hinzufügen, die keine haben. Betrachten Sie die folgenden Erweiterungsmethoden:

public static class NullableExtensions
{
    public static void Deconstruct<T>(
        this T? nullable,
        out bool hasValue,
        out T value) where T : struct
    {
        hasValue = nullable.HasValue;
        value = nullable.GetValueOrDefault();
    }
}

Mit dieser Erweiterungsmethode können alle Nullable<T>-Typen in ein Tupel von (bool hasValue, T value)dekonstruiert werden. Das folgende Beispiel zeigt Code, der diese Erweiterungsmethode verwendet:

DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }

record-Typen

Wenn Sie einen record-Typ unter Verwendung von mindestens zwei Positionsparametern deklarieren, erstellt der Compiler eine Deconstruct-Methode mit einem out-Parameter für jeden Positionsparameter in der record-Deklaration. Weitere Informationen finden Sie unter Positionssyntax für die Eigenschaftsdefinition und Dekonstruktorverhalten in abgeleiteten Datensätzen.

Siehe auch