Compartilhar via


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.
    }
    

    O exemplo anterior é complicado e não é recomendado.

  • Por fim, você pode desconstruir a tupla em variáveis já 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.
    }
    
  • 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 um 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 de gravação cujos valores você escolheu ignorar. Em uma atribuição, você declara um descarte com um caractere de sublinhado ("_"). Você pode descartar quantos valores desejar; um único descarte, _, representa todos os valores descartados.

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# oferece suporte integrado para desconstrução de tipos de tuplas, record 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 nulo. Um parâmetro out na assinatura do método representa cada valor a ser desconstruído. Por exemplo, o seguinte método Deconstruct de uma classe Person retorna o primeiro nome, nome do meio e 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 primeiro nome e um sobrenome.
  • Um primeiro nome, um nome do meio e o nome de família.
  • Um nome, um nome de família, 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. Uma variável chamada "_" representa um descarte. 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 (o primeiro e os nomes de família, a cidade e o estado), mas descarta o nome da família 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 de desconstrução

Se você não for o autor de uma classe, struct ou interface, ainda poderá descontruir objetos desse tipo implementando um ou mais Deconstructmétodos de extensão para retornar os valores nos quais você está 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. O segundo indica a acessibilidade da propriedade. Valores booleanos indicam se a propriedade tem métodos de acesso get e set separados ou diferentes níveis de 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.");
}

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