La sentencia switch en C# 7.0

“Si te quiero es porque sos
Mi amor, mi cómplice y todo,
Y en la calle, codo a codo,
Somos mucho más que dos…”
(Poema de Mario Benedetti)

En una entrada anterior reciente presentamos una de las novedades importantes que introdujo C# 7.0, los llamados patrones (patterns), que ahora pueden utilizarse en el contexto de dos construcciones del lenguaje:

  • El operador is de comprobación de tipos, que ahora acepta también en su parte derecha un patrón, además de un tipo.
  • Las cláusulas case de la sentencia switch, que ahora pueden especificar un patrón de emparejamiento, y no únicamente una constante.

En aquella ocasión mostramos cómo hacer uso de los tres tipos de patrones que incorpora C# 7.0 conjuntamente con el operador is; hoy veremos cómo utilizarlos en el marco de una sentencia switch, algo que en la práctica será mucho más común. Para ello, utilizaremos un sencillo ejemplo que contabiliza e imprime los invitados a una fiesta de empresa a la que se admiten también mascotas:

class MainClass
{
    static void Main(string[] args)
    {
        Process(
            new Employee("Octavio"55"Software Eng."),
            new Person("Diana"21),
            new Pet("Shelly"PetType.DOG11)
        );
    }

    static void Process(params object[] data)
    {
        int people = 0pets = 0;
        foreach (var obj in data)
        {
            switch (obj)
            {
                case null:
                    break;
                case Pet pet:
                    Console.WriteLine($"{pet.Name} [{pet.Type}]");
                    pets++;
                    break;
                case Employee e:
                    Console.WriteLine($"{e.Name} ({e.Position})");
                    people++;
                    break;
                case Person p:
                    Console.WriteLine(p.Name);
                    people++;
                    break;
                default:
                    Console.WriteLine("** Invalid input!");
                    break;
            }
        }
        Console.WriteLine($"Total: {people} people, {pets} pets.");
    }
}

Nótese en primer lugar que la expresión de control de la sentencia switch ya no está limitada a los tipos básicos y las cadenas: la expresión puede ser ahora de cualquier tipo, valor o referencia. La primera cláusula case ejemplifica el primer tipo de patrones que estudiamos la vez anterior, los patrones constantes, que es de hecho lo que estábamos acostumbrados a utilizar en cláusulas case en versiones anteriores de C#; solo que ahora null es una constante tan válida como otra cualquiera.

Las tres cláusulas case a continuación ejemplifican el segundo tipo de patrones, los patrones de tipo, que tienen la forma T x, donde T es un tipo y x un identificador de variable; mediante ellos se comprueba si el valor de la expresión es de tipo T, y en caso afirmativo, se asigna dicho valor a la variable x de tipo T. Téngase en cuenta aquí que el valor null nunca “casa” en estos patrones, por lo que es generalmente necesario codificar explícitamente ese caso, tal y como hemos hecho en el ejemplo.

Dejaremos el uso del tercer tipo de patrones (los patrones var) en la sentencia switch para una próxima entrega, para verla en combinación con otra interesante novedad añadida a esta sentencia en C# 7.0: la cláusula when.

Hay que destacar que con la llegada de C# 7.0 y estas nuevas posibilidades, la sentencia switch en una bestia MUY DIFERENTE de lo que era hasta la versión anterior de C#:

a) En primer lugar, la sentencia se hace mucho más dinámica de lo que fue nunca antes. Ahora los patrones de las cláusulas case se emparejan en tiempo de ejecución. Anteriormente, las cláusulas case solo especificaban constantes (diferentes, añadiría yo), y todo estaba claro ya en tiempo de compilación.

b) En segundo lugar, y estrechamente relacionado con lo anterior, ahora el ORDEN de las cláusulas case en el código fuente es importante; antes era irrelevante.

La semántica de switch ahora consiste, en el caso general, en evaluar secuencialmente (en cascada) cada una de las cláusulas case y ejecutar el bloque de código asociado a la primera cláusula que se satisfaga; cediendo el control al bloque default solamente en caso de que ninguna cláusula case tenga éxito. Dado que en una sentencia switch de versiones anteriores de C# solo había constantes mutuamente excluyentes en las cláusulas case, el compilador tenía múltiples opciones a la hora de traducirla a código de máquina, y la única implementación posible no era una cascada de condicionales, sino varios otros métodos tales como tabla de saltos, búsqueda binaria y otros. Por supuesto, el compilador podría continuar utilizando todas esas técnicas cuando encuentre sentencias switch que no utilicen ninguna de las novedades de C# 7.0.

Por último, y ya para concluir por hoy, señalaremos que en C# 7.0 el compilador sigue haciendo, no obstante lo dicho anteriormente, todo lo que puede por nosotros. Por ejemplo, asumiendo como es de suponer que la clase Employee hereda de Person, nuestro programa de ejemplo no compilará si se intercambian de orden los casos correspondientes en la sentencia switch. Esto es similar a lo que hace el compilador toda la vida en relación con los tipos de excepciones que se especifican en cláusulas catch de un mismo try.


Referencia cultural: Como esta entrada se escribe durante el Día de San Valentín, aprovecho para insertar un fragmento de uno de los muchos lindos poemas de amor de ese gigante de la poesía iberoamericana que es el uruguayo Mario Benedetti.

Patrones en C# 7.0

“Early morning, April four
Shot rings out in the Memphis sky.
Free at last, they took your life
They could not take your pride.”
U2, “Pride (In the Name of Love)” (1984)

Otra de las novedades relevantes que introdujo C# 7.0 son los llamados patrones (patterns). Los patrones permiten, con una sintaxis concisa, comprobar que el valor de una expresión tiene una cierta “forma” y, en caso afirmativo, extraer información de esa expresión. El término “patrón” hace referencia al concepto de casamientoemparejamiento de patrones (pattern matching) tan popular en los lenguajes de programación lógica y funcional; aunque, como intentamos mostrar en un artículo anterior dedicado a la deconstrucción, por el momento las posibilidades que ofrece C# en esta área palidecen con respecto a las de tales lenguajes1.

En C# 7.0, los patrones pueden utilizarse para realzar la expresividad de dos construcciones del lenguaje:

  • El operador is de comprobación de tipos acepta ahora también en su parte derecha un patrón, además de un tipo.
  • Las cláusulas case de la sentencia switch ahora pueden especificar un patrón de emparejamiento en lugar de una constante.

Para los ejemplos de esta entrada nos centraremos en los nuevos usos del operador is; dejaremos los ejemplos de patrones en cláusulas case para nuestras próximas entregas, que estarán dedicadas íntegramente a las nuevas posibilidades que desde C# 7.0 ofrece la sentencia switch.

C# 7.0 incorpora tres tipos de patrones:

a) Los patrones constantes tienen la forma c, donde c es una constante, y comprueban que el valor de la expresión es igual a c. Por ejemplo:

    public static void Patterns1(object oint n)
    {
        if (o is null)
            Console.WriteLine("o is null");
        if (n is 27)
            Console.WriteLine("n is 27");
    }

El argumento de que is es más legible que == no es razón suficiente para convencerme a utilizar este tipo de patrón con el operador is. Tenga en cuenta el lector que la generación de código actual se apoya ciegamente en object.Equals() para la comparación, lo que puede dar lugar a pérdidas de rendimiento (por ejemplo, esto provoca dos operaciones de boxing al ejecutar la segunda sentencia if).

b) Los patrones de tipo tienen la forma T x, donde T es un tipo y x un identificador de variable; mediante ellos se comprueba si el valor de la expresión es de tipo T, y en caso afirmativo, se asigna dicho valor a la variable x de tipo T.

    public static void Patterns2(object o)
    {
        if (o is int n || (o is string s && int.TryParse(sout n)))
            Console.WriteLine(n);
        if (o is Person p)
            Console.WriteLine(p.FullName);
    }

Aquí si hay una economía de expresión importante sin comprometer la legibilidad, en mi humilde opinión. La primera parte del método del ejemplo imprime un valor en la consola si el parámetro suministrado es un entero (en cuyo caso, como “efecto colateral”, el valor entero es asignado a la variable n), o si el parámetro es una cadena que puede interpretarse como un entero (en cuyo caso, la cadena es asignada a la variable s, que es pasada a continuación a TryParse)2.

c) Finalmente, los patrones var tienen la forma var x, donde x es un identificador de variable; estos patrones casan incondicionalmente, y simplemente asignan el valor de la expresión a la variable x, que será del mismo tipo de la expresión.

    public static void Patterns3(object o)
    {
        if (o is var x)
            Console.WriteLine(x.GetType());
    }

Un caso particular a tener muy en cuenta al utilizar esta construcción es que en ella el patrón siempre casa, incluso si la expresión es null. Si el parámetro de entrada es null, el método del ejemplo lanzará NullReferenceException al intentar llamar a GetType(). No se me ocurre ningún caso práctico en el que pudiera ser conveniente utilizar este tipo de patrón con el operador is; sin embargo, en una próxima entrega veremos el caso de uso para el que fue concebido, asociado a una cláusula case.

En las próximas entregas, que estarán dedicadas a la sentencia switch, arrojaremos más luz sobre el uso de estos tres tipos de patrones en las cláusulas case de la misma.

1 Los creadores de C# han prometido en varios foros que en el futuro se añadirán más tipos de patrones al lenguaje.

Nótese que la variable n se considera declarada tras su primera aparición, por lo que puede utilizarse como parámetro de salida de TryParse.


Referencia musical: Como esta entrada se escribe durante un día festivo dedicado a la figura de Martin Luther King, aprovecho para rememorar este tema dedicado a él de una banda imprescindible de las últimas cuatro décadas, U2,  que periódicamente nos demuestra que en el rock no todo son excesos e individualismo, sino que en ocasiones también puede encontrarse progresismo y compromiso social.

Empiece a programar. Un enfoque multiparadigma con C#

A tiempo para Noche Vieja me ha llegado mi copia del recién-publicado “Empiece a programar: un enfoque multiparadigma con C#“, escrito por mi maestro y amigo Miguel Katrib y otros miembros de su equipo. Es, sinceramente, el libro que me gustaría tener a mi alcance si volviera a tener 18 años y empezara mi andadura en el mundo de la programación. Y siendo totalmente honesto, debo reconocer también que aprendí un par de cosas y rememoré muchas otras con la lectura del libro, a pesar de mis 30+ años de experiencia y casi 20 usando C#. No me extiendo más aquí, porque el lector interesado podrá encontrar mi opinión detallada en el Prefacio del libro, junto con la Tabla de Contenidos, disponibles en la página del libro en Amazon.com.

¡Feliz 2018!

Sobre la implementación interna de las tuplas-valor (y 4)

“When the hills of Los Angeles are burning
Palm trees are candles in the murder wind
So many lives are on the breeze
Even the stars are ill at ease
And Los Angeles is burning…”
Bad Religion, Los Angeles is Burning (2004)

Parte 1
Parte 2
Parte 3

Para concluir la serie sobre la implementación interna de las tuplas-valor, hoy presento un pequeño divertimento que vuelve a incidir sobre los detalles de dicha implementación.

Suponga que tiene una tupla-valor con varios elementos, y quiere tratar a dichos elementos como una secuencia, por ejemplo para agregarlos de alguna manera (recuerde que el operador Aggregate de LINQ no está limitado, ni mucho menos,  a secuencias numéricas). Independientemente de que LINQ ya hace tiempo que dejó estar en el centro de atención para convertirse en algo “normal”, estamos en presencia de un caso típico en el que quisiéramos “habilitar para LINQ” un tipo de datos específico, para lo que tan solo hace falta crear un método extensor que reciba una instancia del tipo y produzca a partir de ella un IEnumerable o IEnumerable<T> a recorrer. Es un tema al que dediqué varios artículos en la pasada década, en particular éste. Con un método como el anterior (al que llamaremos, como se recomienda, AsEnumerable), podremos hacer cosas como lo siguiente:

using System;
using System.Linq;
 
namespace ValueTuples
{
    class MainClass
    {
        static void Main()
        {
            var tuple = (12345678910);
            var sum = (from int x in tuple.AsEnumerable()
                       where x % 2 == 0
                       select x).Sum();              
            Console.WriteLine(sum); // Imprime 30
        }
    }
}

La implementación del método extensor que se me ha ocurrido (teniendo en cuenta las cosas que hemos venido hablando sobre la implementación interna de las tuplas-valor) es la que sigue a continuación; se aceptan sugerencias para su mejora.

using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using SC = System.StringComparison;
 
namespace ValueTuples
{
    public static class ValueTupleExtensions
    {
        public static IEnumerable AsEnumerable(this object valueTuple)
        {
            Type type = valueTuple.GetType();
            if (!type.FullName.StartsWith("System.ValueTuple"                                          SC.InvariantCulture))
                throw new InvalidOperationException("Invalid type");
            foreach (FieldInfo fi in type.GetFields().OrderBy(fi => fi.Name))
            {
                if (fi.Name == "Rest"// "El octavo pasajero"
                {
                    foreach (object o in AsEnumerable(fi.GetValue(valueTuple)))
                        yield return o;
                }
                else
                {
                    yield return fi.GetValue(valueTuple);
                }
            }
        }
    }
}

Referencia musical: Como el tiempo para concebir y escribir este post se presentó debido a un incendio forestal que amenazó nuestra empresa hoy hasta el punto de que nos evacuaron a casa, lo menos que pude hacer fue buscar una canción alusiva al tema, y creo que encontré una bastante buena. Bad Religion es una banda de punk-rock formada en Los Ángeles en 1980 (ya yo me estaba haciendo viejo por esa época) y que aún se mantiene activa. No soy un experto en su obra, pero suenan bien y tienen letras inteligentes.

Sobre la implementación interna de las tuplas-valor (3)

En la entrada anterior mostramos cómo el compilador de C# 7.0 y posteriores genera atributos TupleElementNames (espacio de nombres System.Runtime.CompilerServices) para los parámetros de entrada y valores de retorno de métodos que sean tuplas, y cómo estos atributos son luego utilizados por el propio compilador para permitirnos utilizar nombres menos áridos que Item1, Item2, etc. para referirnos a los elementos de cada una de esas tuplas. Esta generación de atributos TupleElementNames no se limita únicamente a los argumentos y valores de retorno de funciones. Por ejemplo, el siguiente programa compila y ejecuta correctamente de C# 7.0 en adelante:

01 using System;
02 class MainClass
03 {
04     static readonly (string Name, int Age) tuple = ("Denis"31);
05
06     static void Main()
07     {
08         (double Re, double Im) i = (01);
09         // Imprime '(0, 1)'
10         Console.WriteLine($"({i.Re}{i.Im})");
11         // Imprime 'Denis is 31'
12         Console.WriteLine($"{tuple.Name} is {tuple.Age}");
13     }
14 }

Si se compila y luego desensambla el código, se verá algo como lo siguiente:

internal class MainClass
{
    // Static Fields
    [TupleElementNames(new string[] { "Name", "Age" })]
    private static readonly ValueTuple<stringint> tuple = 
        new ValueTuple<stringint> ("Denis"31);

    // Static Methods
    private static void Main ()
    {
        ValueTuple<doubledouble> valueTuple = 
            new ValueTuple<doubledouble> (0.01.0);
        Console.WriteLine (string.Format ("({0}, {1})", 
            valueTuple.Item1, valueTuple.Item2));
        Console.WriteLine (string.Format ("{0} is {1}", 
            MainClass.tuple.Item1, MainClass.tuple.Item2));
    }
}

En este caso, el atributo TupleElementNames se aplica a un campo estático, que podría ser público y con ello accesible desde otros ensamblados. Observe, por otra parte, que no se ha generado el atributo para la variable local i; el análisis de flujo del compilador detecta que ésta no podrá ser accedida desde fuera del ámbito en el que ha sido definida, y por lo tanto el atributo sería superfluo.

Personalmente, creo que preferiré siempre que se pueda utilizar un estilo similar al usado en la declaración del campo tuple en el código anterior (línea 04), asignando tipos y nombres explícitos a los elementos de las tuplas del lado izquierdo de la igualdad. Asimismo, utilizaré notación Pascal para esos nombres (como en Name y Age), lo que no solo me gusta más, sino además es lo que recomienda Microsoft para campos públicos en los convenios de nombres de .NET. Ahora bien, ¿qué otras alternativas tenemos a nuestro alcance a la hora de declarar e inicializar un campo o variable de tipo de tupla? Pues podríamos enumerar las siguientes:

a) Tanto para campos como para variables locales, simplemente escribir: (stringint) tupla = (Denis31);

Obviamente, el código cliente tendrá que referirse a los elementos de la dupla utilizando los nombres Item1 e Item2.

b) Para variables locales, utilizar var: var i = (01);

Tal como está escrito el código, también habría que utilizar Item1 e Item2 ; pero ¡siga leyendo!

c) Resulta que la sintaxis para constantes de tuplas permite (por ortogonalidad con los tipos anónimos) establecer nombres para los elementos como parte del literal de tupla; por lo tanto, se obtendría el mismo efecto de la línea 08 del ejemplo al escribir1: var i = (Re: 0.0, Im: 1.0);

Esto sólo funciona para variables locales declaradas mediante var; en otro caso, se produce la advertencia “The tuple element name ‘X‘ is ignored, because a different name or no name is specified for the target type“.

d) Por último, debemos mencionar que C# 7.1 ha añadido un pequeño detalle adicional a lo planteado en el inciso anterior, permitiendo que los nombres de los elementos de las tuplas se infieran a partir de los nombres de las variables utilizadas para inicializar dichos elementos. Por ejemplo, considere el siguiente pequeño programa:

01 using System;
02 class MainClass
03 {
04     static void Main()
05     {
06         double re = 0.0, im = 1.0;
07         var i = (re, im);
08
09         // Imprime '(0, 1)' (en C# 7.1) 
10         Console.WriteLine($"({i.re}{i.im})");
11     }
12 }

Compilado con C# 7.1, el programa anterior ejecuta correctamente, porque los nombres re e im se infieren para los elementos de i. Pero si se compila con C# 7.0, esos elementos no tienen nombre explícito y el compilador se queja en la línea 10 de que i no tiene ningún miembro llamado re, aunque lo hace de una manera muy simpática, como aludiendo al futuro: “Tuple element name ‘re‘ is inferred. Please use version 7.1 or higher to access an element by its inferred name“. Obviamente, este mensaje se modificó cuando C# 7.1 salió a la luz.

La versión del compilador a utilizar se selecciona en las opciones del proyecto:


1 Pero note que deberá incluir el “.0” para las constantes – en caso contrario, sería una dupla con dos elementos enteros.

Sobre la implementación interna de las tuplas-valor (2)

Al final de la entrada anterior mostramos cómo el compilador de C# 7.0 y posteriores sintetiza un atributo TupleElementNames (espacio de nombres System.Runtime.CompilerServices) cada vez que una función devuelve una tupla. Lo mismo ocurre en el caso de que uno o más parámetros de entrada sean tuplas. Por ejemplo, para la siguiente función, que recibe dos parámetros de tipos de tuplas y retorna una tercera:

(string Name, int Age) Transform(
    (string FirstName, string LastName) name, 
    (int Year, int Month, int Day) birthDate)
{
    var fullName = $"{name.FirstName} {name.LastName}";
    var age = Years(
        new DateTime(birthDate.Year, birthDate.Month, birthDate.Day), 
        DateTime.Now);
    return (fullName, age);
}

se genera el siguiente código:

[return: TupleElementNames(new string[] { "Name", "Age" })]
private ValueTuple<stringint> Transform (
    [TupleElementNames(new string[] { "FirstName", "LastName" })]
    ValueTuple<stringstring> name, 
    [TupleElementNames(new string[] { "Year", "Month", "Day" })]
    ValueTuple<intintint> birthDate)
{
    string fullName = string.Format("{0} {1}", name.Item1, name.Item2);
    int age = MainClass.Years(
        new DateTime (birthDate.Item1, birthDate.Item2, birthDate.Item3), 
        DateTime.Now);
    return new ValueTuple<stringint> (fullName, age);
}

Recordando cuál es el objetivo central para el que fueron diseñados los atributos de C#, el de asociar información adicional de metadatos a tipos o miembros de un tipo para que posteriormente pueda ser consumida por herramientas externas1, queda claro por qué razón se asocian estos atributos a las tuplas: para que el propio compilador de C# los lea al procesar tanto un fichero de código fuente como un ensamblado compilado del que no dispongamos del código fuente, y de esta forma se haga posible que se nos permita escribir x.Name o x.Age en lugar de la notación más críptica x.Item1 o x.Item2 (que, en cualquier caso, sigue estando disponible) para acceder a los miembros de la dupla devuelta por Transform.

Observe, sin embargo, que esta implementación mediante atributos de los nombres de elementos nos podría obligar en ciertos casos a escribir código adicional. En el fondo, los verdaderos nombres de los campos son Item1, Item2, etc., y tendremos que personalizar a medida, por ejemplo, el código que hace reflexión o serialización si quisiéramos aprovechar los nombres mnemotécnicos en esos escenarios. Por ejemplo, la serialización directa a JSON de un objeto devuelto por la función anterior utilizando el método JsonConvert.SerializeObject de la librería JSON.NET producirá simplemente:

{"Item1":"Denis","Item2":31}

En nuestra próxima entrega continuaremos hablando un poco más sobre este mismo tema, que ha dado bastante de sí.


1 Generalmente prefiero los términos que se utilizan en C# con respecto a los usados para conceptos similares o equivalentes de Java, pero creo aquí Java se llevó las palmas: allí se habla de anotaciones (annotations) en lugar de atributos.

Sobre la implementación interna de las tuplas-valor

“… ¡Azúcar!”
Exclamación que popularizó la cantante cubana Celia Cruz (la Reina de la Salsa)

Algunas entradas atrás prometimos hablar sobre los mecanismos en los que se apoyan C# 7.0 y posteriores (note el lector que ya está disponible C# 7.2) para dar soporte al nuevo “azúcar sintáctico” que permite hacer uso de las tuplas-valor, y eso es precisamente sobre lo que trataremos hoy aquí.

Ya en aquella ocasión indicábamos que la base fundamental para la característica lo aporta el tipo (o familia de tipos) System.ValueTuple, que se diferencia de la ahora cuasi-obsoleta System.Tuple introducida en .NET Framework 4.0 en varias aspectos fundamentales:

  • ValueTuple es una estructura en vez de una clase, lo que lo hace en principio más ligero y eficiente. En particular, esto hace posible que el compilador aloje las variables locales de ese tipo, así como las tuplas-valor que sean argumentos de entrada y valores de retorno, en la pila (stack) en vez de la memoria dinámica.
  •  Los miembros de una estructura ValueTuple son campos públicos en vez de propiedades de solo lectura, lo que nuevamente permite generar un código más rápido para el acceso a ellos.
  • Como consecuencia de lo anterior, a diferencia de lo que ocurría en el caso de Tuple, los elementos de las tuplas-valor pueden ser modificados (son mutables).
  • Si bien por defecto el convenio de nombres para los elementos de una tupla es el mismo que utiliza Tuple (Item1, Item2, etc.), para las tuplas-valor el lenguaje soporta el uso de nombres alternativos para los campos.

Cuando hablamos de ValueTuple, en realidad nos estamos refiriendo a una familia de ocho estructuras genéricas ValueTuple<T1>, ValueTuple<T1, T2>, ValueTuple<T1, T2, T3>, etc. que se utilizan para trabajar con tuplas-valor de 1, 2, 3, etc. componentes. Para el remoto caso en que se necesite definir tuplas de 8 o más elementos, el “octavo pasajero” en el tipo con ocho parámetros es en realidad otra tupla, lo que nos permitiría extendernos hasta 14 componentes (o más, si se aplicara nuevamente el truco). Por simplicidad, en el código a continuación nos ceñiremos a duplas (o sea, tuplas con dos componentes).  Existe además una versión no genérica de ValueTuple, que ofrece algunos métodos auxiliares de propósito general, y en particular métodos fábrica estáticos Create<T1>, Create<T1, T2>, etc. para la creación de tuplas-valor de los tipos genéricos.

Si se desensamblan los tipos ValueTuple<T1, T2> y ValueTuple utilizando una herramienta adecuada (yo estoy utilizando el Assembly Browser de Visual Studio for Mac, que me ha sorprendido muy favorablemente) se verá algo similar al código a continuación, en el que se muestra prácticamente todo lo que es necesario conocer sobre la implementación interna de las tuplas-valor. Nótese que las tuplas-valor implementan las interfaces de comparación estructural, que hacen posible la comparación y ordenación de entidades de esos tipos basadas en los valores de sus campos.

namespace System
{
    [Serializable]
    [StructLayout (LayoutKind.Auto)]
    public struct ValueTuple<T1, T2> : IValueTupleInternal, ITuple,
        IEquatable<ValueTuple<T1, T2>>, 
        IComparable, IComparable<ValueTuple<T1, T2>>,
        IStructuralEquatable, IStructuralComparable
    {
        // Campos
        public T1 Item1;
        public T2 Item2;
 
        // Constructor
        public ValueTuple (T1 item1, T2 item2) { Item1 = item1; Item2 = item2; }
 
        // Implementación de las interfaces, Equals y GetHashCode
    }

    public struct ValueTuple
    {
        // Fábrica de tuplas de dos elementos
        public static ValueTuple<T1, T2> Create<T1, T2> (T1 item1, T2 item2)
        {
            return new ValueTuple<T1, T2> (item1, item2);
        }

        // ...
    }
}

El código de ejemplo que he preparado para hoy se apoya en la clase Person que hemos venido utilizando anteriormente, y es el siguiente:

01    using System;
02    class MainClass
03    {
04        static Person dh = new Person("Denis"new DateTime(19851227));
05 
06        static void Main()
07        {
08            var d = GetPersonData();
09            // Imprime 'Denis is 31'
10            Console.WriteLine($"{d.Name} is {d.Age}");
11        }
12 
13        static (string Name, int Age) GetPersonData()
14        {
15            int years = Years(dh.BirthDate, DateTime.Now);
16            return (dh.Name, years);
17        }
18 
19        static int Years(DateTime d1, DateTime d2)
20        {
21            return (d2.Year - d1.Year - 1) +
22                (((d2.Month > d1.Month) ||
23                 ((d2.Month == d1.Month) && (d2.Day >= d1.Day))) ? 1 : 0);
24         }
25     }

El siguiente es el código que muestra el desensamblador para dicha clase (solo se presentan las porciones relevantes):

01        private static void Main()
02        {
03            ValueTuple<stringint> d = GetPersonData();
04            Console.WriteLine(string.Format ("{0} is {1}", d.Item1, d.Item2));
05        }
06
07        [return: TupleElementNames (new string[] { "Name", "Age" })]
08        private static ValueTuple<stringint> GetPersonData()
09        {
10            int years = Years(dh.BirthDate, DateTime.Now);
11            return new ValueTuple<stringint>(dh.Name, years);
12        }

Observe cómo el compilador transforma las líneas 08, 10, 13 y 16 del código original en las líneas 03, 04, 08 y 11 del código compilado utilizando los conceptos explicados anteriormente. ¡Azúcar! La única duda que podría quedar es de qué va el atributo generado para el método GetPersonData en la línea 07, pero el propio texto de esa línea y el de la línea 04 seguramente le habrán dado una buena pista. Con ello continuaremos en la próxima entrega, porque ésta ya se ha hecho demasiado larga.


Referencia musical: Celia Cruz fue una excelsa cantante que llevó la música cubana y su amor por Cuba a todos los rincones del planeta y que tuve la gran suerte de conocer personalmente. Su extensa obra ha quedado ampliamente plasmada en múltiples fuentes audiovisuales disponibles. Ahora bien, hace un tiempo que está en Netflix una serie sobre su vida que a mi modo de ver no le hace ninguna justicia: además de incorporar muy pocas de sus canciones (¡y son 80 capítulos!), se basa en un guión verdaderamente atroz que exhuma cursilería, maniqueísmo y frecuentes faltas a la verdad e incorrecciones históricas, entre otros defectos. Yo sólo me la “fumé” por motivos nostálgicos: oír hablar a los personajes interpretados por actores cubanos (especialmente los femeninos, que tanto me recordaban a mi madre). Si usted no se encuentra en mi situación, le recomiendo que se abstenga de verla y aproveche su tiempo de una mejor manera.

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!