Tipos de tupla (referencia de C#)

Disponible en C# 7.0 y versiones posteriores, la característica tuplas proporciona una sintaxis concisa para agrupar varios elementos de datos en una estructura de datos ligera. En el siguiente ejemplo se muestra cómo se puede declarar una variable de tupla, inicializarla y acceder a sus miembros de datos:

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

Como se muestra en el ejemplo anterior, para definir un tipo de tupla, se especifican los tipos de todos sus miembros de datos y, opcionalmente, los nombres de campos. No se pueden definir métodos en un tipo de tupla, pero se pueden usar los métodos proporcionados por .NET, como se muestra en el siguiente ejemplo:

(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

A partir de C# 7.3, los tipos de tupla admiten operadores== de igualdad y !=. Para obtener más información, consulte la sección Igualdad de tupla.

Los tipos de tupla son tipos de valores; los elementos de tupla son campos públicos. Esto hace que las tuplas sean tipos de valor mutables.

Nota

La característica de las tuplas requiere el tipo System.ValueTuple y los tipos genéricos relacionados (por ejemplo, System.ValueTuple<T1,T2>), que están disponibles en .NET Core y .NET Framework 4.7 y versiones posteriores. Para usar tuplas en un proyecto que tenga como destino .NET Framework 4.6.2 o versiones anteriores, agregue el paquete NuGet System.ValueTuple al proyecto.

Puede definir tuplas con un gran número arbitrario de elementos:

var t = 
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26);  // output: 26

Casos de uso de tuplas

Uno de los casos de uso más comunes de tuplas es como un tipo devuelto del método. Es decir, en lugar de definir outlos parámetros del método, puede agrupar los resultados del método en un tipo devuelto de tupla, como se muestra en el ejemplo siguiente:

var xs = new[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

var ys = new[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    var min = int.MaxValue;
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

Como se muestra en el ejemplo anterior, puede trabajar directamente con la instancia de la tupla devuelta o deconstruirla en variables independientes.

También puede utilizar tipos de tupla en lugar de tipos anónimos; por ejemplo, en las consultas LINQ. Para obtener más información, vea Elección entre tipos de tupla y anónimos.

Normalmente, se usan tuplas para agrupar elementos de datos relacionados de forma flexible. Esto suele ser útil en métodos de utilidad privados e internos. En el caso de la API pública, considere la posibilidad de definir un tipo de clase o de estructura.

Nombres de campo de tupla

Puede especificar explícitamente los nombres de campo de tupla en una expresión de inicialización de tuplas o en la definición de un tipo de tupla, como se muestra en el siguiente ejemplo:

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

A partir de C# 7.1, si no se especifica ningún nombre de campo, se puede deducir del nombre de la variable correspondiente en una expresión de inicialización de tupla, como se muestra en el siguiente ejemplo:

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

Esto se conoce como "inicializadores de proyección de tupla". El nombre de una variable no se proyecta en un nombre de campo de tupla en los siguientes casos:

  • El nombre del candidato es un nombre de miembro de un tipo de tupla, por ejemplo, Item3, ToString o Rest.
  • El nombre del candidato es un duplicado de otro nombre de campo de tupla, ya sea explícita o implícita.

En estos casos, se especifica el nombre de un campo o se accede a un campo por su nombre predeterminado.

Los nombres predeterminados de los campos de tupla son Item1, Item2, Item3, etc. Siempre puede usar el nombre predeterminado de un campo, incluso cuando se especifica un nombre de campo de forma explícita o inferida, como se muestra en el siguiente ejemplo:

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

En la asignación de tuplas y las comparaciones de igualdad de tuplas no se tienen en cuenta los nombres de campo.

En el tiempo de compilación, el compilador sustituye los nombres de campo no predeterminados por los nombres predeterminados correspondientes. Como resultado, los nombres de campo especificados o inferidos no están disponibles en el tiempo de ejecución.

Sugerencia

Habilite la regla de estilo de código .NET IDE0037 para establecer una preferencia en nombres de campo de tupla explícitos o inferidos.

Asignación y deconstrucción de tuplas

C# admite la asignación entre tipos de tupla que satisfacen estas dos condiciones:

  • ambos tipos de tupla tienen el mismo número de elementos;
  • para cada posición de tupla, el tipo de elemento de tupla de la derecha es el mismo que el tipo de elemento de tupla de la izquierda correspondiente, o bien puede convertirse a este.

Los valores de elementos de tupla se asignan siguiendo el orden de los elementos de tupla. Los nombres de los campos de tupla se omiten y no se asignan, como se muestra en el siguiente ejemplo:

(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

También puede usar el operador de asignación = para deconstruir una instancia de tupla en variables independientes. Para ello, siga uno de estos métodos:

  • Declarar explícitamente el tipo de cada variable entre paréntesis:

    var t = ("post office", 3.6);
    (string destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Usar la palabra clave var fuera de los paréntesis para declarar las variables con tipo implícito y permitir que el compilador deduzca sus tipos:

    var t = ("post office", 3.6);
    var (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Usar variables existentes:

    var destination = string.Empty;
    var distance = 0.0;
    
    var t = ("post office", 3.6);
    (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    

Para obtener más información sobre la deconstrucción de tuplas y otros tipos, consulte Deconstrucción de tuplas y otros tipos.

Igualdad de tupla

Comenzando con C# 7.3, los tipos de tupla admiten los operadores == y !=. Estos operadores comparan los miembros del operando izquierdo con los miembros correspondientes del operando derecho, siguiendo el orden de los elementos de la tupla.

(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

Como se muestra en el ejemplo anterior, las operaciones == y != no tienen en cuenta los nombres de campo de tupla.

Dos tuplas son comparables cuando se cumplen estas dos condiciones:

  • Ambas tuplas tienen el mismo número de elementos. Por ejemplo, t1 != t2 no se compila si t1 y t2 tienen números diferentes de elementos.
  • Para cada posición de tupla, los elementos correspondientes de los operandos de la tupla de la izquierda y de la derecha son comparables con los operadores == y !=. Por ejemplo, (1, (2, 3)) == ((1, 2), 3) no se compila porque 1 no es comparable con (1, 2).

Los operadores == y != comparan las tuplas en modo de cortocircuito. Es decir, una operación se detiene en cuanto da con un par de elementos que no son iguales o alcanza los extremos de las tuplas. Sin embargo, antes de cualquier comparación, se evalúan todos los elementos de tupla, como se muestra en el siguiente ejemplo:

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
    Console.WriteLine(s);
    return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

Tuplas como parámetros de salida

Normalmente, se refactoriza un método que tiene parámetros de out en un método que devuelve una tupla. Sin embargo, hay casos en los que un parámetro de out puede ser de un tipo de tupla. En el siguiente ejemplo básico se indica cómo trabajar con tuplas como parámetros de out:

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

Tuplas frente a System.Tuple

Las tuplas de C#, que están respaldadas por tipos de System.ValueTuple, son diferentes de las tuplas representadas por tipos de System.Tuple. Las diferencias principales son las siguientes:

  • Los tipos de System.ValueTuple son tipos de valores. Los tipos de System.Tuple son tipos de referencia.
  • Los tipos de System.ValueTuple son mutables. Los tipos de System.Tuple son inmutables.
  • Los miembros de datos de tipos de System.ValueTuple son campos. Los miembros de datos de tipos de System.Tuple son propiedades.

Especificación del lenguaje C#

Para obtener más información, consulte las siguientes notas de propuestas de características:

Vea también