Deconstrucción en C# 7.0: cuidado con las sobrecargas!

“… You’re the reason I’m travelin’ on / Don’t think twice, it’s all right”
Bob Dylan, “Don’t think twice, it’s all right” (1963)

En nuestra entrega anterior presentamos la deconstrucción, un nuevo mecanismo que ofrece C# 7.0 para permitir desintegrar un objeto de cualquier tipo en las partes que lo componen, asignando los valores de esas partes a nuevas variables o a variables ya existentes utilizando la sintaxis incorporada al lenguaje para expresar tipos de tuplas-valor:

    var dh = new Person("Denis", new DateTime(1985, 12, 27));
    // ...
    (string name, int year, int month, int day) = dh;  // declaración
    string s;
    int y, m, d;
    (s, y, m, d) = dh;  // asignación
}

El tipo que ofrece la deconstrucción debe suministrar uno o más métodos Deconstruct con los parámetros de salida adecuados. Alternativamente, Deconstruct puede ser un método extensor (extension method), lo que hace posible utilizar el mecanismo para tipos de los cuales no disponemos del código fuente. Para que el código anterior funcione, la clase Person debe haberse definido así:

using System;
namespace ValueTuples
{
    public class Person
    {
        public string Name { get; }
        public DateTime BirthDate { get; }

        public Person(string name, DateTime birthDate)
        {
            Name = name;
            BirthDate = birthDate;
        }

        public void Deconstruct(out string name,
            out int year, out int month, out int day)
        {
            name = Name;
            year = BirthDate.Year; month = BirthDate.Month; day = BirthDate.Day;
        }
    }
}

Cuando empecé a probar esta característica, hasta aquí todo iba de maravilla. Pero entonces pensé que me gustaría poder devolver además del número del día el día de la semana, y claro, envolver toda la información sobre el día en una tupla, para poder hacer algo así:

    // Deconstrucción a varios niveles???
    (string name, _, _, (_, DayOfWeek dayOfWeek)) = dh;
    // Esto imprime: Denis was born a Friday.
    Console.WriteLine($"{name} was born a {dayOfWeek}.");
}

Cuando se me ocurrió hacer esto, probablemente estaría pensando en la unificación recursiva de Prolog (craso error :-)). La sobrecarga de Deconstruct con cuatro parámetros que permitiría una sintaxis como la anterior podría ser ésta:

    public void Deconstruct(out string name,
        out int year, out int month, 
        out (int DayNumber, DayOfWeek DayOfWeek) day)
    {
        name = Name;
        year = BirthDate.Year; month = BirthDate.Month; 
        day = (BirthDate.Day, BirthDate.DayOfWeek);
    }

Si se comenta la primera sobrecarga de Deconstruct y se incorpora ésta, todo funciona de maravilla. Pero si se activan los dos mecanismos simultáneamente, la clase Person compila correctamente (y por qué no), pero el código cliente que intenta utilizar la segunda sobrecarga es rechazado por el compilador:

The call is ambiguous between the following methods and properties:
    Person.Deconstruct(out string, out int, out int, out int) and
    Person.Deconstruct(out string, out int, out int, 
        out (int DayNumber, DayOfWeek DayOfWeek))

En este momento me quedé un poco desilusionado; esperaba que el compilador sería capaz de detectar que el patrón del lado izquierdo de la asignación utiliza una tupla en la cuarta posición, y por lo tanto la segunda sobrecarga es la única aceptable en ese caso. Buscando una respuesta rápida, planteé la pregunta en StackOverflow, y David Arno gentilmente me la respondió; en su propia respuesta me indica que ha enviado una sugerencia al equipo de desarrollo de C# porque piensa que la resolución de sobrecargas durante la deconstrucción no funciona tan finamente como podría.

Problemas como éste me hacen dudar de si las últimas novedades no se estarán añadiendo al lenguaje a un ritmo demasiado apresurado y sin tiempo para pensar dos veces (think twice) y estudiar todos los posibles casos extremos; de hecho, la especificación formal de C# 7.0 todavía se está escribiendo… ¡y ya salió C# 7.1! Pero tales son los tiempos ágiles que corren, y la mejor documentación de las características del lenguaje es, cada vez más, el código fuente de Roslyn en GitHub.

En cualquier caso, me ha quedado claro que es una mala idea dotar a un tipo de dos o más sobrecargas de Deconstruct con la misma cantidad de parámetros. ¡Queda advertido, estimado lector!


Referencia musical: Durante mi adolescencia, la vía casi exclusiva que tenía para escuchar pop y rock norteamericano eran las emisoras comerciales de la Florida, y Bob Dylan nunca fue especialmente popular por esos lares. Así que realmente solo empecé a conocer su obra bien pasados los treinta, gracias a mis queridos amigos de El Puerto de Santa María. “Don’t think twice…” es, sin dudas, uno de sus temas más reconocibles.

Deconstrucción en C# 7.0

En nuestra entrega anterior, dedicada a las tuplas-valor (value tuples) añadidas recientemente a C# 7.0, mencionamos brevemente el mecanismo de deconstrucción (deconstruction), al que dedicaremos aquí algo más de espacio. La deconstrucción es un nuevo mecanismo sintáctico que aprovecha la sintaxis incorporada al lenguaje para representar tipos de tuplas-valor para permitir, de una manera sencilla y conveniente, descomponer un objeto de cualquier tipo en las partes que lo componen, asignando los valores de esas partes a nuevas variables (en cuyo caso estamos en presencia de una declaración de deconstrucción, deconstructing declaration) o a variables ya existentes (asignación de deconstrucción, deconstructing assignment):

    var dh = new Person("Denis"new DateTime(19851227));
    // ...
    (string name, int year, int month, int day) = dh;  // declaración
    string s;
    int y, m, d;
    (s, y, m, d) = dh;  // asignación
}

Como se habrá dado cuenta el lector, en el fondo se trata de simple azúcar sintáctico que evita tener que hacer uso de múltiples asignaciones y/o llamadas con incómodos parámetros out. Para satisfacer al compilador, es necesario que el tipo a descomponer ofrezca un método Deconstruct con los parámetros adecuados (en él es donde precisamente se encapsulan esos parámetros out). Deconstruct puede incluso ser un método extensor (extension method) que esté en ámbito, lo que hace posible utilizar el mecanismo para tipos de los cuales no disponemos del código fuente. Además, se puede implementar varias sobrecargas para ofrecer varias maneras de deconstruir un objeto. Por ejemplo, el código mostrado anteriormente funciona porque las siguientes definiciones están en vigor:

using System;
namespace ValueTuples
{
    public class Person
    {
        public string Name { get; }
        public DateTime BirthDate { get; }

        public Person(string name, DateTime birthDate)
        {
            Name = name;
            BirthDate = birthDate;
        }

        public void Deconstruct(out string name,
            out int year, out int month, out int day)
        {
            name = Name;
            BirthDate.Deconstruct(out year, out month, out day);
        }
    }

    public static class Extensions
    {
        public static void Deconstruct(this DateTime dateTime,
            out int year, out int month, out int day)
        {
            year  = dateTime.Year;
            month = dateTime.Month;
            day   = dateTime.Day;
        }
    }
}

Visto todo lo anterior, solo resta mencionar algunos detalles adicionales:

  • Las tuplas-valor soportan de manera predefinida la deconstrucción en sus componentes originales sin que tengamos que hacer absolutamente nada; veremos más detalles al respecto en una próxima entrega, que ya estaba prometida :-).
  • Los descartes, que ya presentamos hace algún tiempo, pueden ser útiles en caso de que solo estemos interesados en algunos de los elementos resultantes de la deconstrucción, como se muestra en el siguiente ejemplo:
using System;
namespace ValueTuples
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            var dh = new Person("Denis"new DateTime(19851227));
            // ...
            (string name, int year, _, _) = dh;
            Console.WriteLine($"Name: {name}");
            Console.WriteLine($"Year: {year}");
        }
    }
}

Es precisamente esta aplicación conjunta de la deconstrucción y los descartes lo que tanto me recuerda al lenguaje de programación lógica Prolog. Aunque hay que reconocer que, incluso con todas las novedades añadidas a C# 7.0 y 7.1, C# aún se queda bastante corto: la unificación de Prolog es bidireccional, y las variables (y los descartes) pueden situarse a cualquiera de los dos lados del símbolo de unificación (que no asignación); más aún, el mecanismo de unificación de Prolog se basa en un algoritmo de emparejamiento de patrones (pattern matching) mucho más general, y que no depende para su funcionamiento de que se definan de antemano métodos especializados o se restrinjan en modo alguno las estructuras de las tuplas a unificar.


Agradecimientos: Lo poco que sé sobre Programación Lógica y sus aplicaciones lo aprendí de un MAESTRO con mayúsculas, Don Luciano García Garrido, a quien envío desde aquí un afectuoso saludo.

Tuplas-valor en C# 7.0

“… Pero lo nuestro es pasar / Pasar haciendo caminos / Caminos sobre la mar …”
Joan Manuel Serrat, “Cantares” (1969), basada en  un poema de Antonio Machado

La idea de escribir una entrada relacionada con las tuplas-valor (value tuples) añadidas recientemente a C# 7.0 me trajo a la mente el artículo que escribí una vez para la revista dotNetManía con mi maestro y amigo Miguel Katrib cuando aparecieron las tuplas-referencia, aquellas que se sintetizan a través del uso de la clase genérica Tuple<> que fue incorporada a .NET Framework en la versión 4.0. Las nuevas tuplas-valor se introducen casi que con la idea de hacer obsoletas (“todo pasa y todo queda…“) las tuplas-referencia, resolviendo la mayor parte de los inconvenientes que éstas presentan, y algunas ideas en ese sentido ya las proponíamos en aquel lejano artículo (que el lector interesado puede encontrar, gracias a la generosidad de nuestro gran amigo Paco Marín, aquí).

El primer elemento a destacar es que para poder hacer uso de las tuplas-valor es necesario instalar el paquete de NuGet System.ValueTuple, a menos que esté usted desarrollando un proyecto basado en .NET Framework 4.7 o superior, o en .NET Core 2.0 o superior. Como tipo de datos, una diferencia crucial entre ValueTuple y System.Tuple es que el nuevo tipo es una estructura en vez de una clase y sus miembros son campos en vez de propiedades, lo que lo hace en principio más ligero y eficiente con relación a Tuple. Aparte de eso, un somero análisis de los ensamblados correspondientes le convencerá de que ambos tipos utilizan patrones de desarrollo parecidos (incluyendo el truco del “octavo pasajero” al que aludíamos en el artículo antes mencionado) y ofrecen posibilidades similares. Algunas de ellas pueden verse en la parte inicial del ejemplo que se muestra más abajo, aunque también varias diferencias:

  • Si bien por defecto el convenio de nombres para los elementos de una tupla es el mismo (Item1, Item2, etc.), para las tuplas-valor el compilador soporta el uso de nombres alternativos para los campos.
  • Como los elementos de las tuplas-valor son campos públicos, sus valores pueden ser modificados (son mutables).

Pero sin duda alguna lo más útil e interesante en relación con las tuplas-valor es que el compilador de C# las utiliza al transformar el nuevo “azúcar sintáctico” que ahora está soportado en C# 7.0. Dejaré para una próxima entrada un análisis de los “actos de magia” que utiliza el compilador para generar código basado en ValueTuple (que por supuesto no se basa en la herencia, dado que las estructuras no la soportan).

La segunda parte del código a continuación muestra un ejemplo de la que tal vez sea la aplicación más popular para las tuplas-valor: una función que devuelve varios valores. En este caso, el método FibonacciInterval determina simultáneamente el menor número de Fibonacci que es mayor o igual que el valor recibido como parámetro y el siguiente número de la secuencia. Note como se puede también utilizar nombres explícitos para hacer más legible el código que llame a la función:

using System;

namespace ValueTuples
{
    class MainClass
    {
        static void Main(string[] args)
        {
            var t1 = ValueTuple.Create("DH"ValueTuple.Create(27121985));
            var t2 = ("DH", (27121985)); // equivalente "endulzado"
            t1.Item2.Item2 = 10;
            Console.WriteLine(t1.Item2);

            var t3 = (Nombre: "Diana", Nacimiento: (Día: 2, Mes: 4, Año: 1998));
            Console.WriteLine(t3.Nacimiento.Día);

            for (uint i = 0; i < 50; i++)
                Console.WriteLine($"Fibonacci interval for {i} = " +
                    FibonacciInterval(i));
        }

        public static (uint Min, uint Max) FibonacciInterval(uint number)
        {
            (uint min, uint max) = (01);
            while (true)
            {
                if (number < max)
                    return (min, max);
                uint sum = min + max;
                min = max;
                max = sum;
            }
        }
    }
}

Observe también la simpática asignación (uint min, uint max) = (01) al inicio del método. Se trata de un ejemplo de deconstrucción (del inglés deconstruction) o desmontaje de una tupla-valor en sus componentes individuales. Esta característica de deconstrucción (que es más general y puede aplicarse a tipos que no sean tuplas-valor) me recuerda, como ya he dicho antes, al mecanismo de unificación utilizado en el lenguaje Prolog; la abordaré con más detalle en otra futura entrega.


Referencia musical: Tendría yo unos doce años cuando Joan Manuel Serrat fue a cantar a nuestra escuela en las afueras de La Habana, y desde ese entonces lo cuento entre mis favoritos. Adicionalmente, el hecho de que las letras de sus temas más populares de aquellos tiempos estuvieran basadas en poemas de Antonio Machado y Miguel Hernández me incitó a estudiar la vida y obra de esos poetas, sin saber que un día el destino me llevaría a España. ¡Gracias, Maestro!