Desconstruindo tuplas e outros tipos

Uma tupla fornece uma maneira leve de recuperar vários valores de uma chamada de método. Mas depois de recuperar a tupla, você precisa lidar com seus elementos individuais. Trabalhar elemento por elemento é incômodo, conforme mostra o exemplo a seguir. O método QueryCityData retorna uma tupla de três 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 propriedade e de campo de um objeto pode ser igualmente complicado: é preciso atribuir um valor de campo ou de propriedade a uma variável, membro por membro.

Você pode recuperar vários elementos de uma tupla ou recuperar vários valores calculados, de campo e de propriedade de um objeto em uma só operação deconstruct. Para desconstruir uma tupla, você atribui os elementos dela a variáveis individuais. Quando você desconstrói um objeto, você atribui os elementos dela a variáveis individuais.

Tuplas

O C# conta com suporte interno à desconstrução de tuplas, que permite que você descompacte todos os itens em uma tupla em uma única operação. A sintaxe geral para desconstruir uma tupla é semelhante à sintaxe para definir uma: coloque as variáveis para as 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 tupla de quatro a quatro variáveis separadas:

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

Há três maneiras de desconstruir uma tupla:

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

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

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

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

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

    Isso é difícil 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.
    }
    

Você não pode especificar um tipo específico fora dos parênteses, mesmo se todos os campos na tupla tiverem o mesmo tipo. Isso gera o erro do compilador CS8136, "O formulário de desconstrução 'var (...)' não permite um tipo específico para 'var'.".

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

Elementos tupla com descartes

Geralmente, 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ê opta por ignorar. Um descarte é escolhido por um caractere de sublinhado ("_") em uma atribuição. Você pode descartar tantos valores quantos desejar; todos são representados pelo descarte único, _.

O exemplo a seguir ilustra o uso de tuplas com descartes. O método QueryCityDataForYears a seguir retorna uma tupla de seis com o nome de uma cidade, sua área, um ano, a população da cidade nesse ano, um segundo ano e população da cidade nesse segundo ano. O exemplo mostra a alteração na população entre esses dois anos. Entre os dados disponíveis da tupla, não estamos preocupados com a área da cidade e sabemos o nome da cidade e as duas datas em tempo de design. Como resultado, estamos interessados apenas nos dois valores de população armazenados na tupla e podemos lidar com seus valores restantes como descartes.

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 tupla diferentes dos record tipos e DictionaryEntry . No entanto, como o autor de uma classe, um struct ou uma interface, você pode permitir instâncias do tipo a ser desconstruído implementando um ou mais métodos Deconstruct. 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 método Deconstruct a seguir de uma classe Person retorna o nome, o segundo nome e o sobrenome:

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

Em seguida, você pode desconstruir uma instância da classe Person denominada p com uma atribuição semelhante à seguinte:

var (fName, mName, lName) = p;

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

  • Um nome e um sobrenome.
  • Um nome, nome do 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 métodos Deconstruct com o mesmo número de parâmetros são ambíguos. Você deve ter cuidado ao definir métodos Deconstruct com diferentes números de parâmetros ou "aridade". Métodos Deconstruct 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ê faria com tuplas, você pode usar descartes para ignorar os itens selecionados retornados por um método Deconstruct. Cada descarte é definido por uma variável chamada "_", sendo que uma única operação de desconstrução pode incluir vários descartes.

O exemplo a seguir desconstrói um objeto Person em quatro cadeias de caracteres (os nomes e sobrenomes, 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 criar uma classe, struct ou interface, você ainda poderá decompor objetos desse tipo implementando um ou mais Deconstructmétodos de extensão para retornar os valores nos quais você estiver interessado.

O exemplo a seguir define dois métodos de extensão Deconstruct para a classe System.Reflection.PropertyInfo. O primeiro retorna um conjunto de valores que indicam as características da propriedade, incluindo seu tipo, se ela é estática ou instância, se ela é somente leitura e se é indexada. O segundo indica a acessibilidade da propriedade. Já que a acessibilidade dos acessadores get e set pode ser diferente, valores boolianos indicam se a propriedade acessadores get e set separados e, em caso afirmativo, se eles têm a mesma acessibilidade. Se houver apenas um acessador ou ambos os acessadores get e set têm a mesma acessibilidade, a variável access indica a acessibilidade da propriedade como um todo. Caso contrário, a acessibilidade dos acessadores get e set é indicada pelas variáveis 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

Método de extensão para tipos de sistema

Alguns tipos de sistema fornecem o método Deconstruct como uma conveniência. Por exemplo, o tipo System.Collections.Generic.KeyValuePair<TKey,TValue> fornece essa funcionalidade. Quando você está iterando em 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 método Deconstruct 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();
    }
}

Esse método de extensão permite que todos os tipos Nullable<T> sejam desconstruídos em uma tupla de (bool hasValue, T value). O exemplo a seguir mostra o código que usa este 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 }

Tipos record

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

Confira também