Decostruzione di tuple e altri tipi

Una tupla è un metodo semplice per recuperare più valori da una chiamata a un metodo. Tuttavia dopo aver recuperato la tupla è necessario gestirne i singoli elementi. Se eseguita un elemento alla volta, questa operazione può risultare molto laboriosa, come visualizzato nell'esempio seguente. Il metodo QueryCityData restituisce una tupla con tre elementi e ogni elemento viene assegnato a una variabile in un'operazione separata.

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

Il recupero di più valori di campi e proprietà da un oggetto può essere altrettanto complesso: è necessario assegnare un valore di campo o proprietà a una variabile, un membro alla volta.

È possibile recuperare più elementi da una tupla o recuperare più valori di campi, proprietà e valori calcolati da un oggetto in una singola operazione di decostruzione. Per decostruire una tupla gli elementi corrispondenti vengono assegnati a singole variabili. Quando si decostruisce un oggetto si assegnano valori selezionati a singole variabili.

Tuple

In C# è incluso il supporto per la decostruzione di tuple, che consente di decomprimere tutti gli elementi di una tupla in un'unica operazione. La sintassi generale per la decostruzione di una tupla è simile alla sintassi per la definizione della tupla: le variabili a cui va assegnato ogni elemento vengono racchiuse tra parentesi sul lato sinistro di un'istruzione di assegnazione. Ad esempio l'istruzione seguente assegna gli elementi di una tupla con quattro elementi a quattro variabili distinte:

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

Esistono tre modi per decostruire una tupla:

  • È possibile dichiarare in modo esplicito il tipo di ogni campo all'interno di parentesi. Nell'esempio seguente viene usato questo approccio per decostruire la tupla con tre elementi restituita dal metodo QueryCityData.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • È possibile usare la parola chiave var in modo che C# deduca il tipo di ogni variabile. Posizionare la parola chiave var all'esterno delle parentesi. Nell'esempio seguente viene usata l'inferenza del tipo questo approccio per decostruire la tupla con tre elementi restituita dal metodo QueryCityData.

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

    È anche possibile usare la parola chiave var individualmente con una o con tutte le dichiarazioni di variabili all'interno delle parentesi.

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

    Questo approccio è eccessivamente complesso e non è consigliato.

  • Infine, è possibile decostruire la tupla in variabili che sono già state dichiarate.

    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.
    }
    
  • A partire da C# 10, è possibile combinare la dichiarazione di variabile e l'assegnazione in una decostruzione.

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

Non è possibile specificare un tipo specifico all'esterno delle parentesi, anche se ogni campo nella tupla presenta lo stesso tipo. Questa operazione genera l'errore del compilatore CS8136 "Nel form di decostruzione 'var (...)' non è consentito un tipo specifico per 'var'".

È necessario assegnare ogni elemento della tupla a una variabile. Se si omette un elemento, il compilatore genera l'errore CS8132: "Non è possibile decostruire una tupla di 'x' elementi in 'y' variabili".

Elementi di tupla con eliminazioni

Spesso quando si decostruisce una tupla si è interessati solo ai valori di alcuni elementi. È possibile avvalersi del supporto in C# delle variabili discard, variabili di sola scrittura delle quali si è scelto di ignorare i valori. Una variabile discard viene scelta da un carattere di sottolineatura ("_") in un'assegnazione. È possibile rimuovere il numero di valori desiderato; tutti sono rappresentati dalla variabile discard singola _.

L'esempio seguente illustra l'uso delle tuple con le variabili discard. Il metodo QueryCityDataForYears restituisce una tupla con sei elementi con il nome di una città, l'area della città, un anno, la popolazione della città per tale anno, un secondo anno e la popolazione della città per tale anno. L'esempio visualizza la variazione della popolazione tra questi due anni. Tra i dati resi disponibili dalla tupla non interessa l'area della città, mentre il nome della città e le due date sono già noti in fase di progettazione. Di conseguenza interessano soltanto i due valori di popolazione archiviati nella tupla, mentre gli altri valori possono essere gestiti come variabili discard.

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

Tipi definiti dall'utente

C# non offre il supporto predefinito per la decostruzione di tipi non tupla diversi da tipi record e DictionaryEntry. Tuttavia l'autore di una classe, uno struct o un'interfaccia può consentire la decostruzione di istanze del tipo implementando uno o più metodi Deconstruct. Il metodo restituisce un valore void e ogni valore da decostruire è indicato da un parametro out nella firma del metodo. Ad esempio il seguente metodo Deconstruct di una classe Person restituisce il nome, il secondo nome e il cognome:

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

È quindi possibile decostruire un'istanza della classe Person denominata p con un'assegnazione simile alla seguente codice:

var (fName, mName, lName) = p;

L'esempio che segue implementa l'overload del metodo Deconstruct per restituire varie combinazioni di proprietà di un oggetto Person. I singoli overload restituiscono:

  • Un nome e un cognome.
  • Un nome, un secondo nome e un cognome.
  • Un nome, un cognome, un nome di città e un nome di stato.
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!

Più metodi Deconstruct con lo stesso numero di parametri sono ambigui. È necessario prestare attenzione a definire Deconstruct metodi con diversi numeri di parametri o "arity". Deconstruct metodi con lo stesso numero di parametri non possono essere distinti durante la risoluzione dell'overload.

Tipo definito dall'utente con variabili discard

Come per le tuple, è possibile usare le variabili discard per ignorare elementi selezionati restituiti da un metodo Deconstruct. Ogni variabile discard è definita da una variabile denominata "_" e una singola operazione di decostruzione può includere diverse variabili discard.

L'esempio seguente esegue la decostruzione di un oggetto Person in quattro stringhe (nome, cognome, città e stato) ma rimuove il cognome e lo stato.

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

Metodi di estensione per i tipi definiti dall'utente

Anche un utente che non ha creato una classe, uno struct o un'interfaccia può eseguire la decostruzione di oggetti di questo tipo implementando uno o più metodi di estensione Deconstruct per restituire i valori che risultano di interesse.

L'esempio riportato di seguito illustra due metodi di estensione Deconstruct per la classe System.Reflection.PropertyInfo. Il primo metodo restituisce un set di valori che indicano le caratteristiche della proprietà: il tipo, se è statica o di istanza, se è di sola lettura e se è indicizzata. Il secondo indica l'accessibilità della proprietà. Dato che l'accessibilità delle funzioni di accesso get e set può essere diversa, i valori booleani indicano se la proprietà ha funzioni di accesso get e set separate e in questo caso se tali funzioni presentano la stessa accessibilità. Se è presente solo una funzione di accesso o se le funzioni di accesso get e set hanno la stessa accessibilità, la variabile access indica l'accessibilità della proprietà nel suo complesso. In caso contrario l'accessibilità delle funzioni di accesso get e set è indicata dalle variabili getAccess e 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

Metodo di estensione per i tipi di sistema

Alcuni tipi di sistema forniscono il metodo Deconstruct per praticità. Ad esempio, il tipo System.Collections.Generic.KeyValuePair<TKey,TValue> fornisce questa funzionalità. Quando si esegue l'iterazione su un, System.Collections.Generic.Dictionary<TKey,TValue> ogni elemento è un KeyValuePair<TKey, TValue> e può essere decostruito. Si consideri l'esempio seguente:

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

È possibile aggiungere un metodo Deconstruct ai tipi di sistema che non ne hanno uno. È necessario considerare il seguente metodo di estensione:

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

Questo metodo di estensione consente di decostruire tutti i tipi Nullable<T> in una tupla di (bool hasValue, T value). L'esempio seguente mostra il codice che usa questo metodo di estensione:

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 tipi

Quando si dichiara un tipo di record utilizzando due o più parametri posizionali, il compilatore crea un metodo Deconstruct con un parametro out per ogni parametro posizionale nella dichiarazione record. Per ulteriori informazioni, consultare Sintassi posizionale per la definizione di proprietà e Comportamento del deconstructor nei record derivati.

Vedi anche