Desconstrução de tuplas e outros tipos

Uma tupla fornece uma maneira leve de recuperar vários valores de uma chamada de método. Mas uma vez que você recuperar a tupla, você tem que lidar com seus elementos individuais. Trabalhar elemento a elemento é complicado, como mostra o exemplo a seguir. O QueryCityData método retorna uma tripla e cada um de seus elementos é atribuído a uma variável em uma operação separada.

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

Recuperar vários valores de campo e propriedade de um objeto pode ser igualmente complicado: você deve atribuir um valor de campo ou propriedade a uma variável membro por membro.

Você pode recuperar vários elementos de uma tupla ou recuperar vários campos, propriedades e valores computados de um objeto em uma única operação de desconstrução . Para desconstruir uma tupla, você atribui seus elementos a variáveis individuais. Ao desconstruir um objeto, você atribui valores selecionados a variáveis individuais.

Tuplas

O C# possui suporte interno para desconstruir tuplas, o que permite descompactar todos os itens em uma tupla em uma única operação. A sintaxe geral para desconstruir uma tupla é semelhante à sintaxe para definir uma: você coloca as variáveis às quais cada elemento deve ser atribuído entre parênteses no lado esquerdo de uma instrução de atribuição. Por exemplo, a instrução a seguir atribui os elementos de uma quatro tuplas a quatro variáveis separadas:

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

Existem três maneiras de desconstruir uma tupla:

  • Você pode declarar explicitamente o tipo de cada campo entre parênteses. O exemplo a seguir usa essa abordagem para desconstruir as três tuplas retornadas QueryCityData pelo método.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Você pode usar a var palavra-chave para que o C# infera o tipo de cada variável. Coloque a var palavra-chave fora dos parênteses. O exemplo a seguir usa inferência de tipo ao desconstruir as três tuplas retornadas QueryCityData pelo método.

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

    Você também pode usar a var palavra-chave individualmente com qualquer uma ou todas as declarações de variáveis dentro dos parênteses.

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

    Isso é complicado e não é recomendado.

  • Por fim, você pode desconstruir a tupla em variáveis que já foram declaradas.

    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 partir do C# 10, você pode misturar declaração de variável e atribuição em uma desconstrução.

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

Não é possível especificar um tipo específico fora dos parênteses, mesmo que todos os campos na tupla tenham o mesmo tipo. Isso gera erro de compilador CS8136, "Desconstrução 'var (...)' forma não permite um tipo específico para 'var'.".

Você deve atribuir cada elemento da tupla a uma variável. Se você omitir quaisquer elementos, o compilador gerará o erro CS8132, "Não é possível desconstruir uma tupla de elementos 'x' em variáveis 'y'."

Elementos de tupla com devoluções

Muitas vezes, ao desconstruir uma tupla, você está interessado nos valores de apenas alguns elementos. Você pode aproveitar o suporte do C# para descartes que são variáveis somente gravação cujos valores você escolheu ignorar. Um descarte é escolhido por um caractere de sublinhado ("_") em uma atribuição. Você pode descartar quantos valores quiser; todos são representados pelo descarte único, _.

O exemplo a seguir ilustra o uso de tuplas com devoluções. O QueryCityDataForYears método retorna uma tupla de seis com o nome de uma cidade, sua área, um ano, a população da cidade para esse ano, um segundo ano, e a população da cidade para esse segundo ano. O exemplo mostra a mudança da população entre esses dois anos. Dos dados disponíveis da tupla, não estamos preocupados com a área da cidade, e sabemos o nome da cidade e as duas datas no momento do projeto. Como resultado, estamos interessados apenas nos dois valores populacionais armazenados na tupla e podemos lidar com seus valores restantes como descartáveis.

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

Tipos definidos pelo usuário

O C# não oferece suporte interno para desconstruir tipos não tuplos record diferentes dos tipos e DictionaryEntry . No entanto, como o autor de uma classe, um struct ou uma interface, você pode permitir que instâncias do tipo sejam desconstruídas implementando um ou mais Deconstruct métodos. O método retorna void, e cada valor a ser desconstruído é indicado por um parâmetro out na assinatura do método. Por exemplo, o seguinte Deconstruct método de uma Person classe retorna o nome, o nome do meio e o sobrenome:

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

Em seguida, você pode desconstruir uma instância da Person classe nomeada p com uma atribuição como o código a seguir:

var (fName, mName, lName) = p;

O exemplo a seguir sobrecarrega o Deconstruct método para retornar várias combinações de propriedades de um Person objeto. Sobrecargas individuais retornam:

  • Um nome e um sobrenome.
  • Um nome, meio e sobrenome.
  • Um nome, um sobrenome, um nome de cidade e um nome de estado.
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!

Vários Deconstruct métodos com o mesmo número de parâmetros são ambíguos. Você deve ter cuidado para definir Deconstruct métodos com diferentes números de parâmetros, ou "aridade". Deconstruct métodos com o mesmo número de parâmetros não podem ser distinguidos durante a resolução de sobrecarga.

Tipo definido pelo usuário com descartes

Assim como você faz com tuplas, você pode usar descartáveis para ignorar itens selecionados retornados por um Deconstruct método. Cada descarte é definido por uma variável chamada "_", e uma única operação de desconstrução pode incluir vários descartes múltiplos.

O exemplo a seguir desconstrói um Person objeto em quatro cadeias de caracteres (o nome e o sobrenome, a cidade e o estado), mas descarta o sobrenome e o estado.

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

Métodos de extensão para tipos definidos pelo usuário

Se você não criou uma classe, struct ou interface, ainda pode desconstruir objetos desse tipo implementando um ou mais Deconstruct métodos de extensão para retornar os valores nos quais você está interessado.

O exemplo a seguir define dois Deconstruct métodos de extensão para a System.Reflection.PropertyInfo classe. O primeiro retorna um conjunto de valores que indicam as características da propriedade, incluindo seu tipo, se é estático ou instância, se é somente leitura e se está indexado. O segundo indica a acessibilidade do imóvel. Como a acessibilidade dos acessadores get e set pode diferir, os valores booleanos indicam se a propriedade tem acessadores get e set separados e, se tiver, se eles têm a mesma acessibilidade. Se houver apenas um acessador ou ambos o get e o set accessor tiverem a mesma acessibilidade, a access variável indica a acessibilidade da propriedade como um todo. Caso contrário, a acessibilidade dos acessadores get e set são indicadas getAccess pelas variáveis 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

Método de extensão para tipos de sistema

Alguns tipos de sistema fornecem o Deconstruct método como uma conveniência. Por exemplo, o System.Collections.Generic.KeyValuePair<TKey,TValue> tipo fornece essa funcionalidade. Quando você está iterando sobre um System.Collections.Generic.Dictionary<TKey,TValue> , cada elemento é um KeyValuePair<TKey, TValue> e pode ser desconstruído. Considere o seguinte exemplo:

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

Você pode adicionar um Deconstruct método aos tipos de sistema que não têm um. Considere o seguinte método de extensão:

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

Este método de extensão permite que todos os Nullable<T> tipos sejam desconstruídos em uma tupla de (bool hasValue, T value). O exemplo a seguir mostra o código que usa esse método de extensão:

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 tipos

Quando você declara um tipo de registro usando dois ou mais parâmetros posicionais, o compilador cria um Deconstruct método com um out parâmetro para cada parâmetro posicional na record declaração. Para obter mais informações, consulte Sintaxe posicional para definição de propriedade e comportamento do desconstrutor em registros derivados.

Consulte também