Deconstruir tuplas y otros tipos

Una tupla proporciona una manera ligera de recuperar varios valores de una llamada de método. Pero una vez que recupere la tupla, deberá controlar sus elementos individuales. Trabajar elemento a elemento es complicado, como se muestra en el ejemplo siguiente. El método QueryCityData devuelve una tupla de tres y cada uno de sus elementos se asigna a una variable en una operación aparte.

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 varios valores de campo y propiedad de un objeto puede ser igualmente complicado: debe asignar un valor de campo o propiedad a una variable miembro a miembro.

Puede recuperar varios elementos de una tupla o recuperar varios campos, propiedades y valores calculados de un objeto en una sola operación de deconstrucción. Para deconstruir una tupla, asigne sus elementos a variables individuales. Cuando se deconstruye un objeto, los valores seleccionados se asignan a variables individuales.

Tuplas

C# incluye compatibilidad integrada para deconstruir tuplas, lo que permite desempaquetar todos los elementos de una tupla en una sola operación. La sintaxis general para deconstruir una tupla es parecida a la sintaxis para definirla, ya que las variables a las que se va a asignar cada elemento se escriben entre paréntesis en el lado izquierdo de una instrucción de asignación. Por ejemplo, la siguiente instrucción asigna los elementos de una tupla de cuatro a cuatro variables distintas:

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

Hay tres formas de deconstruir una tupla:

  • Se puede declarar explícitamente el tipo de cada campo entre paréntesis. En el ejemplo siguiente se usa este enfoque para deconstruir la tupla de tres que devuelve el método QueryCityData.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Puede usar la palabra clave var para que C# deduzca el tipo de cada variable. Debe colocar la palabra clave var fuera de los paréntesis. En el ejemplo siguiente se usa la inferencia de tipos al deconstruir la tupla de tres devuelta por el método QueryCityData.

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

    También se puede usar la palabra clave var individualmente con alguna de las declaraciones de variable, o todas, dentro de los paréntesis.

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

    Esto es complicado y no se recomienda.

  • Por último, puede deconstruir la tupla en variables que ya se hayan declarado.

    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 de C# 10, puede mezclar la declaración y la asignación de variables en una deconstrucción.

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

No se puede especificar un tipo específico fuera de los paréntesis aunque cada campo de la tupla tenga el mismo tipo. Al hacerlo, se genera el error del compilador CS8136: "La forma de deconstrucción "var (...)" no permite un tipo específico para "var'".

Debe asignar cada elemento de la tupla a una variable. Si omite algún elemento, el compilador genera el error CS8132: "No se puede deconstruir una tupla de "x" elementos en "y" variables".

Elementos de tupla con descartes

A menudo, cuando se deconstruye una tupla, solo interesan los valores de algunos elementos. Puede aprovechar la compatibilidad de C# con los descartes, que son variables de solo escritura cuyos valores se decide omitir. Un carácter de subrayado ("_") elige un descarte en una asignación. Puede descartar tantos valores como quiera; todos se representan mediante el descarte único _.

En el ejemplo siguiente se muestra el uso de tuplas con descartes. El método QueryCityDataForYears devuelve una tupla de seis con el nombre de una ciudad, su área, un año, la población de la ciudad en ese año, un segundo año y la población de la ciudad en ese segundo año. En el ejemplo se muestra la evolución de la población entre esos dos años. De los datos disponibles en la tupla, no nos interesa la superficie de la ciudad, y conocemos el nombre de la ciudad y las dos fechas en tiempo de diseño. Como resultado, solo nos interesan los dos valores de población almacenados en la tupla, y podemos controlar los 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 por el usuario

C# no ofrece compatibilidad integrada con la deconstrucción de tipos que no son de tupla distintos de los tipos record y DictionaryEntry. A pesar de ello, como autor de una clase, una estructura o una interfaz, puede permitir que las instancias del tipo se deconstruyan mediante la implementación de uno o varios métodos Deconstruct. El método no devuelve ningún valor, y cada valor que se va a deconstruir se indica mediante un parámetro out en la firma del método. Por ejemplo, el siguiente método Deconstruct de una clase Person devuelve el nombre de pila, el segundo nombre y los apellidos:

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

A continuación, puede deconstruir una instancia de la clase Person denominada p con una asignación como el código siguiente:

var (fName, mName, lName) = p;

En el ejemplo siguiente se sobrecarga el método Deconstruct para devolver varias combinaciones de las propiedades de un objeto Person. Las sobrecargas individuales devuelven lo siguiente:

  • El nombre de pila y los apellidos.
  • El nombre de pila, el segundo nombre y los apellidos.
  • El nombre de pila, los apellidos, el nombre de la ciudad y el nombre del 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!

Varios métodos de Deconstruct que tienen el mismo número de parámetros son ambiguos. Debe tener cuidado al definir métodos Deconstruct con distintos números de parámetros o "aridad". No se pueden distinguir los métodos Deconstruct con el mismo número de parámetros durante la resolución de sobrecarga.

Tipo definido por el usuario con descartes

Tal como haría con las tuplas, puede usar descartes para omitir los elementos seleccionados que haya devuelto un método Deconstruct. Cada descarte se define mediante una variable denominada "_". Una operación de deconstrucción única puede incluir varios descartes.

En el siguiente ejemplo se deconstruye un objeto Person en cuatro cadenas (el nombre propio, los apellidos, la ciudad y el estado), pero se descartan los apellidos y el 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 extensión para tipos definidos por el usuario

Aunque usted no haya creado una clase, una estructura o una interfaz, puede igualmente deconstruir objetos de ese tipo. Para ello, implemente uno o varios métodos de extensiónDeconstruct que devuelvan los valores que le interesen.

En el ejemplo siguiente se definen dos métodos de extensión Deconstruct para la clase System.Reflection.PropertyInfo. El primero devuelve un conjunto de valores que indican las características de la propiedad, incluido su tipo, si es estática o de instancia, si es de solo lectura y si está indexada. El segundo indica la accesibilidad de la propiedad. Dado que la accesibilidad de los descriptores de acceso get y set puede diferir, los valores booleanos indican si la propiedad tiene descriptores de acceso get y set independientes y, si es así, si tienen la misma accesibilidad. Si solo hay un descriptor de acceso o tanto el descriptor de acceso get como set tienen la misma accesibilidad, la variable access indica la accesibilidad de la propiedad en su conjunto. En caso contrario, la accesibilidad de los descriptores de acceso get y set se indica mediante las variables getAccess y 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 extensión para tipos de sistema

Algunos tipos de sistema proporcionan el método Deconstruct por motivos prácticos. Por ejemplo, el tipo System.Collections.Generic.KeyValuePair<TKey,TValue> proporciona esta funcionalidad. Al recorrer en iteración un objeto System.Collections.Generic.Dictionary<TKey,TValue>, cada elemento es un valor KeyValuePair<TKey, TValue> y se puede deconstruir. Considere el ejemplo siguiente:

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

Puede agregar un método Deconstruct a tipos de sistema que no tengan uno. Considere el siguiente método de extensión:

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 extensión permite que todos los tipos Nullable<T> se deconstruyan en una tupla de (bool hasValue, T value). En el ejemplo siguiente se muestra el código que usa este método de extensión:

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

Cuando se declara un tipo de registro con dos o más parámetros posicionales, el compilador crea un método Deconstruct con un parámetro out para cada parámetro posicional en la declaración de record. Para obtener más información, vea Sintaxis posicional para la definición de propiedades y Comportamiento de deconstructores en registros derivados.

Vea también