11/09/2019Artículo original
A lo largo de la vida de un programador, a veces se encuentra con dilemas de diseño a la hora de escribir el código. Sin ir más lejos, seguramente alguna vez te has hecho la pregunta con la que titula este artículo.
Por ejemplo, imagina que estás creando un método que te gustaría que devolviese un bool (verdadero o falso) para indicar si la ejecución ha sido correcta, pero además quieres obtener un valor de retorno como resultado de la ejecución. De este modo podrías utilizar el valor booleano para saber si todo ha ido bien, y en caso de ser así, trabajar con el retorno, algo como esto:
Llamada a un método Si ha ido bien Ejecuto algo con el resultado si ha ido mal Muestro un mensaje
Para conseguir algo como lo anterior, la mayoría de las veces acabaremos con una de estas 2 opciones:
- Hacer un método que retorne bool y que tenga un parámetro por referencia (ref) o de salida (out)
- Escribir una clase que agrupe los dos resultados y que sea lo que devuelve la función
Cada opción tiene sus ventajas y sus inconvenientes. Por ejemplo, si utilizas la primera de las estrategias, tienes que tener en cuenta que la variable en el exterior va a cambiar en el mismo momento que cambia dentro de método sin esperar a que termine de ejecutarse el método completo ya que la referencia está fuera el método. Esto puede provocarte un serio problema si estás trabajando con código en paralelo, como se puede observar en este ejemplo:
class Program{ static void Main(string[] args) { int valor = 0; Task.Run(async () => { await Task.Delay(300); Console.WriteLine($"El valor de la variable es {valor}"); }); var resultado = MetodoReferencia(ref valor); Console.WriteLine("Ha terminado el método por referencia"); valor = 0; Task.Run(async () => { await Task.Delay(300); Console.WriteLine($"El valor de la variable es {valor}"); }); resultado = MetodoParametro(out valor); Console.WriteLine("Ha terminado el método por parámetro de salida"); Console.Read(); } static bool MetodoReferencia(ref int salida) { salida++; //Simulamos cierta carga en el método Thread.Sleep(1000); return true; } static bool MetodoParametro(out int salida) { salida = 11; //Simulamos cierta carga en el método Thread.Sleep(1000); return true; }}
En este caso, el resultado que cabría esperar es que la salida por consola fuese que el valor de la variable es 0, que es su valor inicial y el método desde el que se modifica todavía no ha terminado de ejecutarse. Pero precisamente por lo que hemos dicho un poco más arriba, la cruda realidad es que la salida es algo como esto:
En cambio, si utilizas una clase o estructura para devolver los datos, desaparece el problema de que las variables cambien en momentos inesperados, pero por contra vas a tener que crear clases/estructuras para cada tipo de retorno que necesites, aunque solo lo vayas a usar una vez, en una única función.
Las tuplas al rescate
Desde la versión 7.0 de C#, se introdujo una nueva característica del lenguaje llamada tupla (tuple en inglés). Si bien es cierto que ya existían antes, en C# 7 se cambiaron para convertirlas en lo que son hoy en día.
Mediante las tuplas podemos conseguir lo mejor de las dos opciones anteriores: no tenemos que definir una clase para que el valor cambie cuando acaba nuestro método.
Para definir una tupla de “n” valores simplemente los envolvemos entre paréntesis y los separamos por comas. Un ejemplo de uso podría ser este:
class Program{ static void Main(string[] args) { var resultado = MetodoTupla(); if (resultado.Resultado) { Console.WriteLine(resultado.Valor); } } static (bool Resultado, int Valor) MetodoTupla() { if (/*Condiciones del código*/) { return (true, 10); } else { return (false, 0); } }}
En este caso la función devuelve una tupla (varios valores a la vez) y por lo tanto se envuelven esos valores de retorno en paréntesis y separados por comas, dándoles además un nombre:
static (bool Resultado, int Valor) MetodoTupla(){ ....}
y se devuelven los resultados con return haciendo lo mismo:
return (true, 10);
También podríamos definir el método sin darle nombre a la salida, así:
static (bool, int) MetodoTupla()
y funcionaría exactamente igual, con la salvedad de que para acceder a sus propiedades tendremos que utilizar los nombres automáticos Item1, Item2, …, Itemn:
class Program{ static void Main(string[] args) { var resultado = MetodoTupla(); if (resultado.Item1) { Console.WriteLine(resultado.Item2); } } static (bool, int) MetodoTupla() { if (/*Condiciones del código*/) { return (true, 10); } else { return (false, 0); } }}
Y por último, también tenemos la posibilidad de utilizar variables ya existentes que “colocamos en la tupla” para asignar la salida:
class Program{ static void Main(string[] args) { var resultado = false; var valor = 0; (resultado, valor) = MetodoTupla(); if (resultado) { Console.WriteLine(valor); } } static (bool, int) MetodoTupla() { if (/*Condiciones del código*/) { return (true, 10); } else { return (false, 0); } }}
A esta última operación se la conoce con el nombre de deconstrucción de tuplas. De hecho podemos también obviar alguno de los valores devueltos si no lo necesitamos gracias al operador guión bajo.
La parte mala de las tuplas, es que no tienen compatibilidad hacia atrás, y solo están disponibles de manera nativa desde .Net Framework 4.7 y .Net Core 2.0 en adelante (.Net Standard lo dispone desde la versión 1.0), y con versiones del lenguaje C# de la 7.0 en adelante.
De todos modos, si nuestro proyecto es .Net Framework 4.5 o superior, o es .Net Core, existen un paquete Nuget que nos permite poder utilizar las tuplas sin ningún problema. Para referencia te dejo esta tabla de compatibilidad:
Soporte.Net Framework.Net Core.Net StandardNativo>= 4.7.0>= 2.0>= 1.0Nuget>= 4.5>= 1.0>= 1.0
En resumen
Aunque siempre una clase a medida puede ser más legible si la puedes reutilizar en varios métodos, si vas a tener que definir varias clases que no se van a utilizar más que en un solo sitio, estarías añadiendo mucho ruido al proyecto y dificultando su lectura. Ahí es en donde entran en juego las tuplas ya que te vas a ahorrar tiempo codificando clases que solo vas a usar una o dos veces como retorno de métodos, y encima facilitas su lectura.