Una tupla agrupa un número pequeño y fijo de valores en un solo valor sin declarar una clase para ello. ("Ana", 31) es una tupla de un string y un int. Las tuplas son la forma idiomática de devolver dos o tres cosas desde un método y de guardar pares de vida corta dentro de un método.
Crear una tupla
El tipo también se escribe con paréntesis: (string, int) es un tipo tupla cuyo primer elemento es un string y el segundo un int.
Salida:
Ana is 31
(3, 4)
179.70
(Ana, 32)
Sin nombres, los elementos son Item1, Item2, Item3 y así sucesivamente. Estas tuplas son structs System.ValueTuple, así que asignar una a otra variable copia los valores, y cambiar la copia no toca el original.
Elementos con nombre
Item1 no dice nada sobre lo que es el valor. Da nombres a los elementos, en el tipo o en el literal:
Salida:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
Los nombres son una comodidad de tiempo de compilación. En tiempo de ejecución, las dos tuplas de arriba son valores ValueTuple<string, int> normales, y por eso compila asignar person a una tupla con otros nombres: solo tienen que coincidir los tipos de los elementos y su orden.
C# 7.1 y posteriores también deducen los nombres de las variables del literal, así que var t = (name, age); te da t.name y t.age sin escribirlos dos veces. En C# 7.0 esos elementos son solo Item1 e Item2.
Devolver varios valores desde un método
Esta es la razón principal por la que se añadieron las tuplas al lenguaje. Antes, las opciones eran los parámetros out, una clase de un solo uso, o System.Tuple con sus anónimos Item1.
Salida:
Lowest 60, highest 95
Average 78.75
Los nombres de los elementos del tipo de retorno pasan a ser los nombres que ve quien llama. Compara el mismo método con parámetros out:
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
Los parámetros out siguen siendo la convención para la forma de TryParse, donde un bool dice si funcionó y el valor vuelve a su lado. Para "calcular varias cosas y devolverlas", una tupla se lee mejor. Consulta ref y out.
Deconstrucción
La deconstrucción desempaqueta una tupla en variables separadas en una sola sentencia:
Salida:
Lima 24.5
Lima
24.5
2 1
Los nombres de las variables en var (city, temp) son tuyos; no tienen que coincidir con los nombres de los elementos de la tupla, porque la deconstrucción va por posición. El intercambio (a, b) = (b, a) construye primero una tupla con los valores antiguos y después asigna, así que no se pierde ningún valor.
Tus propias clases pueden admitir la deconstrucción declarando un método Deconstruct con parámetros out. Desde C# 7 con .NET Core 2.0 o posterior, KeyValuePair también tiene uno, así que un bucle sobre un diccionario puede desempaquetar cada entrada en su cabecera:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
var (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
La misma forma foreach (var (a, b) in listOfTuples) funciona con una lista de tuplas. Tomar la tupla entera como una sola variable de bucle y leer sus elementos con nombre, como hace la siguiente sección, funciona igual de bien.
Tuplas en listas, LINQ y diccionarios
Una lista de tuplas con nombre es una forma ligera de guardar filas de datos dentro de un método:
Salida:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple implementa Equals y GetHashCode elemento a elemento, así que (2, 7) construida en dos sitios distintos encuentra la misma entrada del diccionario. Eso convierte a la tupla en la clave compuesta más sencilla: sin clase, sin función hash escrita a mano, sin concatenar strings como row + ":" + col.
Igualdad de tuplas
Equals compara elemento a elemento y funciona desde que se introdujeron las tuplas:
Salida:
True
False
C# 7.3 añadió == y != para tuplas, que compilan a la misma comparación elemento a elemento y también ignoran los nombres:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple frente a ValueTuple
.NET 4 introdujo System.Tuple, una clase que se crea con Tuple.Create. Sigue en el framework y sigue apareciendo en código y API antiguos, por eso las búsquedas de "C# tuple" suelen llevar a ella.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| Sintaxis | (1, "a"), (int, string) | Tuple.Create(1, "a"), Tuple<int, string> |
| Tipo | struct (tipo de valor) | clase (tipo de referencia) |
| Nombres de elementos | Sí | No, solo Item1, Item2 |
| Mutable | Sí, los elementos son campos | No, propiedades de solo lectura |
| Deconstrucción | Sí | Sí, mediante métodos de extensión |
Salida:
Ben 25
(Ben, 25)
ToValueTuple() y ToTuple() convierten entre las dos. Prefiere las tuplas de valor en el código nuevo: no reservan nada en el heap y pueden llevar nombres.
Cuándo usar una clase o un record
Las tuplas funcionan mejor cuando la agrupación es local y evidente: un método devuelve dos números, una consulta LINQ lleva un par al siguiente paso, un diccionario necesita una clave de dos partes. Cambia a un tipo con nombre cuando:
- La misma forma aparece en varias firmas de métodos públicos.
(string, string, int)en cinco sitios es una clase que todavía no se ha escrito. - Los valores necesitan comportamiento, validación, o más de tres o cuatro elementos.
- Los datos cruzan un límite como la serialización. Los nombres no existen en tiempo de ejecución, así que Newtonsoft.Json escribe
Item1eItem2, ySystem.Text.Jsonescribe{}porque los elementos de una tupla son campos, que por defecto se salta.
Los records de C# 9 te dan casi toda la comodidad de una tupla con un nombre de verdad. record Score(string Name, int Points); trae incorporados la igualdad por valor, un ToString legible y la deconstrucción. Consulta records.
Errores comunes
- Esperar que los nombres sobrevivan en tiempo de ejecución. La reflexión y los serializadores ven
Item1,Item2(o nada en absoluto). - Modificar una copia. Las tuplas son structs:
var t2 = t1; t2.Item1 = 5;dejat1sin cambios. - Tuplas con muchos elementos. Pasados tres o cuatro, un tipo con nombre se lee mejor.
- Confundir el orden de la deconstrucción. Es posicional: con una tupla
(Name, Age),var (age, name) = person;pone el nombre enage, y comovardeduce los tipos, nada te avisa.
Preguntas frecuentes
¿Cómo devuelvo varios valores desde un método en C#?
Devuelve una tupla: declara el tipo de retorno como (int Min, int Max) y escribe return (lowest, highest);. Quien llama lee result.Min y result.Max, o deconstruye con var (min, max) = MinMax(data);. Las alternativas son los parámetros out, que encajan con el patrón TryParse, o una clase pequeña o un record cuando el resultado tiene un significado propio.
¿Qué es una tupla con nombre en C#?
Una tupla cuyos elementos tienen nombres en lugar de Item1, Item2: (string Name, int Age) person = ("Ana", 31); te permite escribir person.Name. Los nombres solo existen en tiempo de compilación; por debajo sigue siendo un ValueTuple<string, int>, y Item1 sigue funcionando.
¿Qué diferencia hay entre Tuple y ValueTuple en C#?
System.Tuple (de .NET 4) es una clase: se reserva en el heap, es inmutable y sus elementos solo pueden ser Item1, Item2. System.ValueTuple (C# 7) es un struct con campos mutables, admite nombres de elementos y la sintaxis (a, b), y es lo que crea cualquier literal de tupla. Usa tuplas de valor en el código nuevo.
¿Cómo funciona la deconstrucción de tuplas en C#?
var (name, age) = person; declara dos variables y les asigna en orden los elementos de la tupla. También puedes asignar a variables existentes con (a, b) = (b, a), que las intercambia, y saltarte elementos con el descarte _: var (_, age) = person;.
¿Puedo comparar tuplas con == en C#?
Desde C# 7.3, == y != comparan las tuplas elemento a elemento: (1, "a") == (1, "a") es true. Los nombres se ignoran; solo importan las posiciones y los valores. Antes de C# 7.3, usa Equals, que las tuplas de valor implementan de la misma forma.