Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=8226ddf3-48db-413d-8e67-be7ead40d528

C# y .NET: Tuplas y cómo devolver más de un objeto como retorno de una función

Imagen ornamental, unos muñecos de lego atados a la misma placa de base, representando una tupla. Foto de freestocks.org

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:

La imagen muestra el resultado, en el que se ve el valor 1 para la primera llamda y 11 para la segunda, en ambos casos antes de terminar el método

El código anterior es un poco rebuscado, pero ejemplifica bien el problema cuando se paraleliza. Básicamente es una llamada a un método que va a cambiar la variable de salida instantáneamente y luego va a tardar 1 segundo en completarse. En paralelo (Task.Run), tras solo 300 milisegundos se comprueba la variable de salida del método para confirmar que ya se ha asignado dentro del método y no al terminar este.

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.

Las tuplas son estructuras de datos ligeros que contienen varios campos para representar los miembros de datos. Fuente:MSDN

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.

Las 3 opciones anteriores son perfectamente válidas, aunque en aras de mantenibilidad y legibilidad, yo recomiendo utilizar la primera ya que cada propiedad tiene un nombre claramente definido y si cambia el método porque añade y/o reordena la tupla, no nos va a afectar precisamente porque accedemos a través del nombre, no de su ubicación en la tupla.

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 Standard
Nativo >= 4.7.0 >= 2.0 >= 1.0
Nuget >= 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.

Fecha de publicación:
Jorge Turrado Jorge lleva en el mundo de la programación desde los tiempos de .Net Framework 3.0. Es experto en la plataforma .NET, en Kubernetes y en técnicas de integración continua entre otras cosas. Actualmente trabaja como Staff SRE en la empresa SCRM Lidl International Hub. Microsoft lo ha reconocido como MVP en tecnologías de desarrollo, es CNCF Ambassador y maintainer oficial de KEDA, el autoescalador de Kubernetes basado en eventos. Puedes seguirlo en Twitter: @JorgeTurrado o en su blog FixedBuffer Ver todos los posts de Jorge Turrado
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (6) -

Puedo utilizar las tuplas regresando, como en el ejemplo un boleano, un entero y una lista ???

Responder

campusMVP
campusMVP

Hola Eder:

Pues devolver lo que necesites, no hay problema. Simplemente procura no abusar de esto y utilizarlo siempre en lugar de clases o estructuras cuando, en realidad, lo vas a utilizar mucho. Como dice el artículo, esta característica está pensada para no tener que definir clases o estructuras de un solo uso solo para devolver resultados de funciones, y no deja de ser una especie de "azúcar sintáctico".

Saludos.

Responder

Ivan Montilla
Ivan Montilla

Hola, en el último comentario de CampusMVP se dice que es una especie de "azucar sintáctico". ¿Exactamente qué hace por detrás? ¿Crea un objeto anónimo?

Un saludo

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola Iván:

Si descompilas a IL el resultado, verás que por detrás utiliza un System.Tuple (www.campusmvp.es/.../...-valores-relacionados.aspx) o un System.ValueTuple o alguna de sus variantes, dependiendo del caso.

Saludos.

Responder

muchas gracias.

Responder

Gracias muy util la info de este articulo

Responder

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.