Tipos de tupla (referencia de C#)

La característica de 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.

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.

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:

int[] xs = [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

int[] ys = [-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.");
    }

    // Initialize min to MaxValue so every value in the input
    // is less than this initial value.
    var min = int.MaxValue;
    // Initialize max to MinValue so every value in the input
    // is greater than this initial value.
    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. En las API públicas, considere la posibilidad de definir un tipo de clase o de estructura.

Nombres de campo de tupla

Especifique explícitamente los nombres de campos 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}.");

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

Eso 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 los casos anteriores, se especifica explícitamente 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 sobre los nombres de campo de tupla explícitos o inferidos.

A partir de C# 12, puede especificar un alias para un tipo de tupla con una directiva using. En el ejemplo siguiente se agrega un alias global using para un tipo de tupla con dos valores enteros para un valor permitido Min y Max:

global using BandPass = (int Min, int Max);

Después de declarar el alias, puede usar el nombre BandPass como alias para ese tipo de tupla:

BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");

Un alias no introduce un nuevo tipo, sino que solo crea un sinónimo para un tipo existente. Puede deconstruir una tupla declarada con el alias BandPass igual que con su tipo de tupla subyacente:

(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");

Al igual que con la asignación o deconstrucción de tuplas, los nombres de miembros de tuplas no necesitan coincidir; los tipos sí.

Del mismo modo, un segundo alias con los mismos tipos de miembros y aridad se puede usar indistintamente con el alias original. Puede declarar un segundo alias:

using Range = (int Minimum, int Maximum);

Puede asignar una tupla Range a una tupla BandPass. Al igual que con todas las asignaciones de tupla, los nombres de campo no deben coincidir, solo los tipos y la aridad.

Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");

Un alias para un tipo de tupla proporciona más información semántica cuando se usan tuplas. No introduce un nuevo tipo. Para proporcionar seguridad de tipos, debe declarar un posicional record en su lugar.

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. Existen muchas maneras de hacerlo:

  • 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.
    
  • 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.
    
  • Declare algunos tipos explícitamente y otros tipos implícitamente (con var) entre paréntesis:

    var t = ("post office", 3.6);
    (var destination, double 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.
    

El destino de una expresión de deconstrucción puede incluir variables existentes y variables declaradas en la declaración de deconstrucción.

También puede combinar la deconstrucción con coincidencia de patrones para inspeccionar las características de los campos de una tupla. En el ejemplo siguiente se recorren varios enteros y se imprimen los que se pueden dividir en 3. Deconstruye el resultado de la tupla de Int32.DivRem y coincide con un Remainder de 0:

for (int i = 4; i < 20;  i++)
{
    if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
    {
        Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
    }
}

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

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 más información, consulte:

Consulte también