Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 14 de noviembre de 2023
C#

Desde su llegada con la versión 7 del lenguaje C#, allá por 2017, nuestro lenguaje favorito dispone de soporte para tuplas. Sin embargo, no he visto muchos proyectos donde estén siendo utilizadas de forma habitual; quizás sea porque pueden hacer el código menos legible, o quizás por desconocimiento, o simplemente porque lo que aportan podemos conseguirlo normalmente de otras formas y preferimos hacerlo como siempre para no sorprender al que venga detrás a tocar nuestro código.

Pero bueno, en cualquier caso, es innegable que las tuplas han venido para quedarse, así que en este post vamos a ver algunos usos posibles, y a veces curiosos, de esta característica del lenguaje C#.

1. Realizar asignaciones múltiples de forma más concisa

Las tuplas permiten realizar asignaciones a múltiples variables de forma muy compacta y sencilla. Un caso de uso simple podemos verlo en el siguiente ejemplo, donde usamos una tupla para declarar e inicializar tres variables:

// Esto:
(int i, string s, bool b) = (42, "Hey", true);
Console.WriteLine($"{i}, {s}, {b}"); // 41, Hey, true

// ...es exactamente lo mismo que esto:
var i = 42;
var s = "Hey";
var b = true;
Console.WriteLine($"{i}, {s}, {b}"); // 41, Hey, true

La opción con tuplas no es mucho más concisa que la clásica porque aún no hemos exprimido toda su potencia. Pero podemos reducirla fácilmente:

// Simplificación 1- > Usamos la inferencia de tipos para declarar con "var":
(var i, var s, var b) = (42, "Hey", true);

// Simplificación 2 -> Sacamos de los paréntesis el factor común:
var (i, s, b) = (42, "Hey", true);

También podemos usar tuplas para asignar variables o propiedades existentes, como vemos en el siguiente ejemplo:

int a = 1;
int b = 2;
...

(a, b) = (5, 8);
Console.WriteLine(a + "," + b); // 5,8

2. Intercambio de variables

Como un escenario específico de uso del punto anterior, tenemos el intercambio (swapping) de variables, algo que desde muy pequeñitos nos enseñaron a hacer utilizando una variable de apoyo:

int a = 1, b = 2;
Console.WriteLine("Before: " + a + "-" + b); // Before: 1-2

int c = a;
a = b;
b = c;
Console.WriteLine("After: " + a + "-" + b); // After: 2-1

Las tuplas, gracias a sus posibilidades de asignación múltiple, nos permite realizarlo de un plumazo:

int a = 1, b = 2;
Console.WriteLine("Before: " + a + "-" + b); // Before: 1-2

(a, b) = (b, a);
Console.WriteLine("After: " + a + "-" + b); // After: 2-1

3. Minimización de métodos

También podríamos minimizar o simplificar métodos gracias a la capacidad de asignación múltiple de las tuplas. Por ejemplo, en el siguiente bloque de código podemos ver cómo reducimos el código necesario para implementar el constructor de una clase que lo único que hace es recoger los parámetros entrantes para almacenarlos en propiedades:

class Person
{
    public readonly string Name { get; }
    public readonly int Age { get; }
    public readonly string Email { get; }

    public Person(string name, int age, string email) => 
        (Name, Age, Email) = (name, age, email);
}

En C# más clásico, el código equivalente sería el siguiente:

class Person
{
    public readonly string Name { get; }
    public readonly int Age { get; }
    public readonly string Email { get; }

    public Person(string name, int age, string email) 
    {
        Name = name;
        Age = age;
        Email = email;
    }
}

Personalmente no es que me guste demasiado esta opción, pero bueno, la posibilidad existe y está bien conocerla.

4. Devolver más de un valor desde métodos o funciones

Esta es una de las utilidades más conocidas de las tuplas. Esto nos puede venir bien para evitar los parámetros de salida (out o ref) en métodos o funciones, y para ahorrarnos la creación de clases o estructuras de datos creadas exclusivamente para guardar temporalmente los valores retornados.

Veamos un ejemplo muy simple. El siguiente método GetMinAndMax() retorna los valores mínimos y máximos de un array de enteros, utilizando parámetros out:

void GetMinAndMax(int[] numbers, out int min, out int max)
{
    min = int.MaxValue;
    max = int.MinValue;

    foreach (int number in numbers)
    {
        min = Math.Min(min, number);
        max = Math.Max(max, number);
    }
}

Pero como sabemos, los parámetros out o ref son molestos de utilizar y no están disponibles en métodos asíncronos, así que muchas veces optamos por introducir tipos específicos para retornar los valores:

public class MinAndMax
{
    public int Min { get; set; }
    public int Max { get; set; }
}

MinAndMax GetMinAndMax(int[] numbers)
{
    min = int.MaxValue;
    max = int.MinValue;

    foreach (int number in numbers)
    {
        min = Math.Min(min, number);
        max = Math.Max(max, number);
    }
    return new MinAndMax() { Min = min, Max = max };
}

Pero vaya pereza, ¿no? Pues en estos casos es donde las tuplas pueden echarnos una mano:

(int Min, int Max) GetMinAndMax(int[] numbers)
{
    int min = int.MaxValue;
    int max = int.MinValue;

    foreach (int number in numbers)
    {
        min = Math.Min(min, number);
        max = Math.Max(max, number);
    }
    return (min, max);
}

De esta forma, el consumidor del método sería idéntico a si hubiésemos utilizado una clase o estructura para el retorno:

var result = GetMinAndMax(arrayOfIntegers);
Console.WriteLine(result.Min +"-"+ result.Max);

5. Claves múltiples de diccionarios

Otro uso no demasiado conocido de las tuplas es actuar como claves en tipos diccionario, lo cual es interesante cuando queremos tener acceso a los elementos utilizando más de un valor como identificador único.

Este escenario solemos solucionarlo mediante una expresión que de alguna forma concatena los distintos campos que componen la clave, creando un valor único. Por ejemplo, si tuviéramos un diccionario de facturas que usara como clave el año de su emisión y su número de serie, podríamos implementarlo así:

// invoiceDictionary es de tipo Dictionary<string, Invoice>
var invoice = invoiceRepository.GetInvoice(42);
invoiceDictionary.Add(invoice.Year + "-" + invoice.SerialNumber, invoice);

Sin embargo, con tuplas queda bastante más claro y no tenemos que evaluar expresiones o consumir allocations a la hora de generar la clave:

// invoiceDictionary es de tipo Dictionary<(int year, int serialNumber), Invoice>
var invoice = GetInvoice(42);
invoiceDictionary.Add((invoice.Year, invoice.SerialNumber), invoice);

Internamente esto es posible porque las tuplas implementan automáticamente los métodos GetHashCode() y Equals() basándose en el valor de sus elementos.

6. Deconstrucción de objetos

Hace mucho tiempo ya hablamos en el blog de la deconstrucción de tuplas y clases en C#. Básicamente, la idea es que los objetos pueden ser descompuestos en un conjunto de valores que pueden ser recogidos posteriormente en forma de tuplas.

Para esto, podemos implementar el método Deconstruct() en cualquier tipo de datos. Este método permite introducir un número indeterminado de parámetros de salida, que son los valores en los que se descompone el objeto. Como vemos en el siguiente ejemplo, la clase Person es deconstruida en tres variables de salida (name, age e email) que básicamente representan el estado completo de los objetos de este tipo:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
 
    public void Deconstruct(out string name, out int age, out string email)
    {
        name = Name;
        age = Age;
        email = Email;
    }
}

Más adelante, podemos obtener estos valores desde una tupla de forma muy sencilla, como se puede observar a continuación:

var person = _peopleRepository.GetPersonById(3422);
...
var (name, age, email) = person;
Console.WriteLine($"I'm {name}, {age} years old");
Console.WriteLine($"Contact me at {email}");

7. Definir matrices multidimensionales

Otra curiosidad de las tuplas es que pueden ser utilizadas para representar matrices multidimensionales. Aunque está bien saber que existe esta posibilidad, la verdad es que no creo que tenga demasiada utilidad práctica porque el resultado es aparentemente muy similar a los arrays, pero con menos posibilidades a la hora de gestionarlos:

var matrix = (
    (1, 2, 3),
    (4, 5, 6),
    (7, 8, 9)
);

A diferencia de los arrays, los elementos no pueden ser accedidos mediante índices, sino que debemos usar los valores de las tuplas:

Console.WriteLine(matrix.Item2.Item3); // Item en fila 2, columna 3: 6

Otra diferencia es que, al no tratarse de una estructura fija, en realidad podríamos introducir estas matrices lo que queramos, porque los elementos de las tuplas son independientes unos de otros:

var matrix = (
    (  1, 'a',     3),
    (1.5,   5, "Six"),
    (  7,   8,  true)
);
Console.WriteLine(matrix.Item2.Item3); // Muestra: Six

8. Uso con pattern matching

También podemos utilizar pattern matching para buscar coincidencias en valores en los elementos de las tuplas de forma muy sencilla. Por ejemplo, en el siguiente bloque de código utiliza la coincidencia de patrones en un bloque switch para mostrar distintos resultados en función de los valores de los elementos:
var name = Console.ReadLine();
var grade = int.Parse(Console.ReadLine());
ShowGrade((name, grade));

void ShowGrade((string, int) student)
{
    switch (student)
    {
        case ("Bill Gates", _):
            Console.WriteLine("You are Bill Gates, so it doesn't matter");
            break;
        case (_, >= 5):
            Console.WriteLine("You are a good student");
            break;
        case (_, < 5):
            Console.WriteLine("You are not a good student");
            break;
    }
}

Publicado en Variable not found.

2 Comentarios:

Juan dijo...

Lo que se aprende por aquí...

No tenía idea de que pudieran usar para asignar valores múltiples.

Lo del clave múltiple en el diccionario me lo encontré hace tiempo y acabé usando un string que concatenaba las claves. Esto no me volverá a pasar ;-)

El único uso que le doy a las tuplas es la de evitar devolver un objeto sencillo. Normalmente un (bool ok, string msg) para representar si todo a ido bien y un mensaje de texto descriptivo que indique si todo ha ido bien o los errores que se han encontrado.

Saludos.

José María Aguilar dijo...

Pues ya ha valido la pena escribir el post :)

Muchas gracias por comentar!