Dekonstrukcja krotek i innych typów

Krotka zapewnia uproszczony sposób pobierania wielu wartości z wywołania metody. Ale po pobraniu krotki musisz obsłużyć jego poszczególne elementy. Praca na podstawie elementu po elemerytmie jest kłopotliwa, jak pokazano w poniższym przykładzie. Metoda QueryCityData zwraca trójkropek, a każdy z jego elementów jest przypisywany do zmiennej w oddzielnej operacji.

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

Pobieranie wielu wartości pól i właściwości z obiektu może być równie kłopotliwe: należy przypisać pole lub wartość właściwości do zmiennej na podstawie składowej.

Można pobrać wiele elementów z krotki lub pobrać wiele pól, właściwości i obliczonych wartości z obiektu w jednej operacji dekonstrukcji . Aby zdekonstruować krotkę, należy przypisać jej elementy do poszczególnych zmiennych. Podczas dekonstrukcji obiektu należy przypisać wybrane wartości do poszczególnych zmiennych.

Krotki

Funkcje języka C# obsługują wbudowaną obsługę dekonstrukcji krotek, co pozwala rozpakować wszystkie elementy w krotki w ramach jednej operacji. Ogólna składnia dekonstrukcji krotki jest podobna do składni definiującej jedną: należy ująć zmienne, do których każdy element ma zostać przypisany w nawiasach po lewej stronie instrukcji przypisania. Na przykład następująca instrukcja przypisuje elementy czwórki do czterech oddzielnych zmiennych:

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

Istnieją trzy sposoby dekonstrukcji krotki:

  • Można jawnie zadeklarować typ każdego pola wewnątrz nawiasów. W poniższym przykładzie użyto tego podejścia, aby zdekonstruować trzy krotki zwrócone przez metodę QueryCityData .

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Możesz użyć słowa kluczowego var , aby język C# wnioskował o typie każdej zmiennej. Słowo kluczowe należy var umieścić poza nawiasami. W poniższym przykładzie użyto wnioskowania typu podczas dekonstrukcji trójkropka zwróconej przez metodę QueryCityData .

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

    Możesz również użyć słowa kluczowego var indywidualnie z dowolnymi lub wszystkimi deklaracjami zmiennych wewnątrz nawiasów.

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

    Jest to kłopotliwe i nie jest zalecane.

  • Na koniec można dekonstrukcji krotki w zmienne, które zostały już zadeklarowane.

    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.
    }
    
  • Począwszy od języka C# 10, można mieszać deklarację zmiennej i przypisanie w dekonstrukcji.

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

Nie można określić określonego typu poza nawiasami, nawet jeśli każde pole w krotki ma ten sam typ. W ten sposób generuje błąd kompilatora CS8136" "Dekonstrukcja "var (...)" nie zezwala na określony typ "var".

Należy przypisać każdy element krotki do zmiennej. Jeśli pominięto jakiekolwiek elementy, kompilator generuje błąd CS8132" "Nie można zdekonstruować krotki elementów "x" w zmiennych "y".

Elementy krotki z odrzuconymi elementami

Często podczas dekonstrukcji krotki interesuje Cię tylko niektóre elementy. Możesz skorzystać z obsługi odrzucań języka C#, które są zmiennymi tylko do zapisu, których wartości zostały zignorowane. Odrzucenie jest wybierane przez znak podkreślenia ("_") w przypisaniu. Możesz odrzucić dowolną liczbę wartości; wszystkie są reprezentowane przez pojedyncze odrzucenie, _.

Poniższy przykład ilustruje użycie krotki z odrzuceniami. Metoda QueryCityDataForYears zwraca sześciokątną z nazwą miasta, jego obszarem, rokiem, populacją miasta w tym roku, drugim rokiem i populacją miasta w tym drugim roku. W przykładzie pokazano zmianę populacji między tymi dwoma latami. Z danych dostępnych z krotki, jesteśmy niezkonsekwowani obszarem miasta i znamy nazwę miasta i dwie daty w czasie projektowania. W związku z tym interesuje nas tylko dwie wartości populacji przechowywane w krotki i mogą obsługiwać pozostałe wartości jako odrzucenia.

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

Typy definiowane przez użytkownika

Język C# nie oferuje wbudowanej obsługi dekonstrukcji typów innych niż recordtypy SłownikEntry . Jednak jako autor klasy, struktury lub interfejsu można zezwolić na dekonstrukcję wystąpień typu przez zaimplementowanie co najmniej jednej Deconstruct metody. Metoda zwraca wartość void, a każda wartość do dekonstrukcji jest wskazywana przez parametr out w podpisie metody. Na przykład następująca Deconstruct metoda Person klasy zwraca pierwszą, środkową i nazwisko:

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

Następnie można zdekonstruować wystąpienie Person klasy o nazwie p przy użyciu przypisania, takiego jak następujący kod:

var (fName, mName, lName) = p;

Poniższy przykład przeciąża metodę Deconstruct , aby zwrócić różne kombinacje właściwości Person obiektu. Zwracane są poszczególne przeciążenia:

  • Imię i nazwisko.
  • Imię, środkowe i nazwisko.
  • Imię, nazwisko, nazwa miasta i nazwa stanu.
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!

Wiele Deconstruct metod o tej samej liczbie parametrów jest niejednoznacznych. Należy zachować ostrożność podczas definiowania Deconstruct metod o różnych liczbach parametrów lub "arity". Deconstruct metody o tej samej liczbie parametrów nie mogą być rozróżniane podczas rozpoznawania przeciążenia.

Typ zdefiniowany przez użytkownika z odrzuceniami

Podobnie jak w przypadku krotek, można użyć odrzucań, aby zignorować wybrane elementy zwrócone przez metodę Deconstruct . Każde odrzucenie jest definiowane przez zmienną o nazwie "_", a pojedyncza operacja dekonstrukcji może zawierać wiele odrzuconych.

Poniższy przykład dekonstrukuje Person obiekt na cztery ciągi (imię i nazwisko, miasto i stan), ale odrzuca nazwisko i stan.

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

Metody rozszerzeń dla typów zdefiniowanych przez użytkownika

Jeśli nie utworzono klasy, struktury lub interfejsu, nadal można dekonstrukcji obiektów tego typu przez zaimplementowanie co najmniej jednej Deconstructmetody rozszerzenia, aby zwrócić interesujące Cię wartości.

W poniższym przykładzie zdefiniowano dwie Deconstruct metody rozszerzenia dla System.Reflection.PropertyInfo klasy . Pierwszy zwraca zestaw wartości, które wskazują cechy właściwości, w tym jej typ, zarówno statyczny, jak i wystąpienie, czy tylko do odczytu, i czy jest indeksowany. Drugi wskazuje dostępność właściwości. Ponieważ dostępność metod pobierania i ustawiania metod dostępu może się różnić, wartości logiczne wskazują, czy właściwość ma oddzielne metody pobierania i ustawiania metod dostępu, a jeśli tak, czy mają te same ułatwienia dostępu. Jeśli istnieje tylko jedno akcesorium lub zarówno metodę get, jak i zestaw dostępu mają taką samą dostępność, access zmienna wskazuje dostępność właściwości jako całości. W przeciwnym razie dostępność metod dostępu get i set są wskazywane przez getAccess zmienne i setAccess .

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

Metoda rozszerzenia dla typów systemowych

Niektóre typy systemów zapewniają metodę Deconstruct jako wygodę. Na przykład System.Collections.Generic.KeyValuePair<TKey,TValue> typ zapewnia tę funkcję. Podczas iteracji nad każdym elementem System.Collections.Generic.Dictionary<TKey,TValue> jest element KeyValuePair<TKey, TValue> i może być zdekonstrukowany. Rozważmy następujący przykład:

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

Możesz dodać metodę Deconstruct do typów systemowych, które ich nie mają. Rozważmy następującą metodę rozszerzenia:

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

Ta metoda rozszerzenia umożliwia dekonstrukcję wszystkich Nullable<T> typów w krotkę klasy (bool hasValue, T value). W poniższym przykładzie pokazano kod, który używa tej metody rozszerzenia:

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 Typy

W przypadku deklarowania typu rekordu przy użyciu co najmniej dwóch parametrów pozycyjnych kompilator tworzy metodę Deconstruct z parametrem out dla każdego parametru pozycyjnego record w deklaracji. Aby uzyskać więcej informacji, zobacz Składnia pozycyjna definicji właściwości i Zachowanie dekonstruktora w rekordach pochodnych.

Zobacz też