El modificador in para parámetros de entrada

“… Someone’s knocking at the door
Somebody’s ringing the bell
Do me a favor, open the door, and let ‘em in…”
Paul McCartney and Wings, “Let ‘Em In” (1976)

Aunque ya todos estamos esperando la presentación oficial de Visual Studio 2019 y C# 8.0 el próximo martes, yo aún sigo descubriendo algunas de las novedades del lenguaje que fueron incluidas en las tres releases puntuales que se liberaron bajo la etiqueta 7.x. En particular, hoy quiero referirme al modificador in, que ahora se puede asociar a los parámetros de un método de manera similar a como se utilizan ref y out en C# desde el principio de los tiempos.

Básicamente, cuando un parámetro se declara in, el compilador impedirá que dicho parámetro pueda modificarse dentro del cuerpo del método. Se trata de un paso pequeño, pero importante, en la dirección de la especificación declarativa de las propiedades de los métodos, principalmente útil en el caso de los parámetros que son clases (y por tanto se pasan por referencia) y podrían modificarse de manera inadvertida. Por otra parte, el hecho de que el parámetro no va a ser modificado hace en principio irrelevante si éste es pasado por valor o por referencia; esto, a su vez, hace posible la generación del código más eficiente posible en el lugar de las llamadas. Éste es el escenario de mayor relevancia en relación con el rendimiento, específicamente en el caso de los parámetros de tipo struct (que son tipos-valor) cuyo tamaño en memoria exceda el tamaño de la palabra del ordenador. En tales casos, el compilador generará código para pasar el parámetro por referencia y no por valor, con el consiguiente ahorro en la copia de datos en la pila. Un ejemplo podría ser el siguiente:

        // Computes the distance from a to b.
        public static double GetDistance(
            in System.Drawing.Point a, 
            in System.Drawing.Point b) 
        { 
            int dx = a.X - b.X; 
            int dy = a.Y - b.Y; 
            return System.Math.Sqrt(dx*dx + dy*dy); 
        }

Este ejemplo me trae a la mente una demo gráfica que solía hacer Microsoft por allá por el año 2000 cuando .NET estaba naciendo. La idea subyacente era que en .NET System.Drawing.Point es una estructura, mientras que en Java (el rival a batir por aquella época) el tipo correspondiente es (o era, no sé si la cosa habrá cambiado con el tiempo) una clase; un algoritmo que tenga que generar dinámicamente muchos puntos será bastante más lento en el segundo caso, debido a la necesidad de gestionar la creación en el heap de numerosos objetos. El uso del nuevo modificador in de C# 7.2 habría contribuido en aquel momento a hacer aún mayor la ventaja.

Personalmente, la inclusión del modificador in en el lenguaje me agrada mucho porque de alguna manera completa la transición de C# de un lenguaje más primitivo en el que los parámetros se denotan pensando en cómo los argumentos correspondientes serán pasados en tiempo de ejecución (por valor o por referencia), como ocurría en lenguajes históricos como C o Pascal, en un lenguaje más moderno y declarativo en el que los parámetros se denotan pensando en su propósito (de entrada, salida o entrada/salida), dejando al compilador los detalles de cómo implementar las llamadas de la manera más eficiente. Ada fue probablemente el lenguaje en el que esta idea se presentó claramente por primera vez.

El uso del modificador in se me antoja imprescindible cuando se esté creando una nueva aplicación o librería; como ocurre con casi todas las características que han sido incluidas después de la concepción original del lenguaje (lo que en inglés se diría un afterthought), debe utilizarse con cierta precaución al incorporarla en proyectos existentes; consulte los documentos de Microsoft al respecto (como éste) para más información.


Referencia musical: “Let ‘Em In” fue el segundo single del quinto disco de Paul McCartney con la banda Wings (“Wings at the Speed of Sound“, 1976). Sonó bastante por la radio, aunque no tanto como “Silly Love Songs“, que fue el número 1 de la lista Billboard de ese año.

De Span a Memory

“…Memoria, memoria
(no, I don’t have a gun)…”
Nirvana, “Come as You Are” (1992)

En nuestra entrada anterior presentamos la clase genérica Span<T>, que fue introducida oficialmente con C# 7.2, seguramente por el hecho de aprovecha algunas novedades relativamente menores incorporadas por primera vez al lenguaje en las distintas actualizaciones de la versión 7 (en particular, los valores de retorno por referencia – ref returns, de los que hemos hablado aquí, y las nuevas posibilidades que ofrece stackalloc, que pueden consultarse en esta página de JetBrains). Span<T> y su homólogo de solo lectura ReadOnlySpan<T> nos permiten hacer referencia de una manera uniforme a cualquier zona de memoria contigua de datos homogéneos, independientemente de dónde esa memoria está situada y de cómo ha sido obtenida.

La vez anterior no mencionamos que Span<T> tiene un pequeño inconveniente: a causa de su implementación (el lector interesado encontrará información detallada en los documentos a los que ya hemos hecho referencia), las instancias de Span<T>, si bien pueden apuntar a cualquier zona arbitraria de memoria, ellas mismas sólo pueden residir en la pila (stack) en forma de variables locales o parámetros de entrada de funciones (siempre que éstas no sean asíncronas, como veremos más adelante). Por ejemplo, si se trata de compilar la siguiente clase:

using System;
public class ThisWontCompile
{
    private Span<int> _span;
}

se obtendrá el error “Field or auto-implemented property cannot be of type ‘Span<int>’ unless it is an instance member of a ref struct (CS 8345)“.

Aquí es donde entra a jugar su papel la clase Memory<T>, cuyas instancias sí pueden residir en el heap. Memory<T> no implementa una API tan amplia como Span<T>, pero sí ofrece una propiedad Span que devuelve un Span<T> asociado a la misma zona de memoria.

Un caso que podría presentársele con cierta frecuencia y en el que deberá cambiar Span<T> por Memory<T> es al crear métodos asíncronos que reciban un span. Recuerde que al “desenrollar” un método asíncrono el compilador genera una clase para capturar el estado local y los parámetros de entrada (el mecanismo se describe muy bien aquí). En el caso que nos ocupa, la clase generada tendría que tener un campo de tipo Span<T>, que como hemos visto antes es imposible.

Suponga que quisiéramos hacer asíncrono el método QuickSort de nuestra entrada anterior. Con tan solo añadir al método el modificador async obtendríamos el error “Parameters or locals of type ‘Span<int>’ cannot be declared in async methods or lambda expressions (CS 4012)“:

  public static async void QuickSort<T>(Span<T> span) where T: IComparable

Podríamos resolver el problema así:

  public static async void QuickSort<T>(Memory<T> memory) where T: IComparable 
  {
      int i = 0, j = memory.Length - 1;
      T pivot = memory.Span[(memory.Length - 1) / 2];

      while (i <= j)
      {
          while (memory.Span[i].CompareTo(pivot) < 0) i++;
          while (memory.Span[j].CompareTo(pivot) > 0) j--;
          if (i <= j)
          {
              T tmp = memory.Span[i];
              memory.Span[i] = memory.Span[j];
              memory.Span[j] = tmp; 

              i++; j--;
          }
      }

      // Llamadas recursivas
      if (j > 0)
          await QuickSort<T>(memory.Slice(0, j + 1));
      if (i < memory.Length - 1)
          await QuickSort<T>(memory.Slice(i, memory.Length - i));
  }

Este es un caso relativamente bastante sencillo, pero en general (como hemos dicho ya alguna vez aquí) siempre que se trabaja directamente con la memoria se debe ser sumamente cuidadoso. Si piensa utilizar estas clases en contextos más avanzados, asegúrese de leer antes las recomendaciones oficiales de Microsoft sobre el uso de Memory<T> y Span<T>.


Referencia musical: A principios de los ’90 dejé de oír rock (tuve que concentrarme seriamente en buscarme la vida ), así que no me enteré de quién era Nirvana hasta más de una década después, cuando hice mi primer viaje a Seattle y Microsoft nos llevó a visitar el excelente Museum of Pop Culture, que exhibe numerosos objetos memorables relacionados con el grupo. “Come as You Are” fue el segundo single del disco “Nevermind“, que los lanzó a la fama. Puedo dar fe de que el tema se sigue escuchando aún con cierta frecuencia en las emisoras de radio de rock clásico.

Generalización del acceso a memoria con Span

“…as the links span our endless caresses
For the freedom of life everlasting…”
Yes, “The Revealing Science of God” (1973)

¡Feliz 2019! Deseo de corazón a todos mis lectores que hayan tenido una buena despedida del año que recién terminó. “Año nuevo, vida nueva”, decía mi abuela Guillermina, y uno de mis propósitos para este año es recuperar la sistematicidad en la escritura, que perdí completamente a finales del año pasado.

Esta primera entrada del año, que llevaba en el tintero unos cuantos meses sin que yo hubiera logrado terminarla, trata sobre el tipo de datos Span<T> (espacio de nombres System.Memory), de relativamente reciente aparición: fue incorporado a .NET Core 2.1. Para utilizarlo en proyectos de .NET Framework, sin embargo, tendrá que importar de Nuget el paquete System.Memory, como puede verse en la captura de pantalla a continuación. Como es muy pronto para empezar a quejarse ya, dejaré las quejas al respecto para el final.

Span<T> es un nuevo tipo por valor (value type) que nos permitirá hacer referencia de una manera uniforme a cualquier zona de memoria contigua de datos homogéneos, independientemente de si esta zona de memoria pertenece a un objeto de código manejado tradicional residente en el heap (T[]), a un buffer nativo (T*) o array creado en la pila mediante stackalloc, o incluso una estructura de datos a la que hayamos obtenido una referencia IntPtr mediante la interacción con código no manejado. A partir de cualquiera de esas fuentes se puede crear una variable de tipo Span<T> (o su pariente de solo lectura ReadOnlySpan<T>). Aparte del hecho de que Span<T> garantiza un acceso con validación de rango a cualquiera de los elementos de la estructura a través de un índice, como si de un array se tratara (s[i]), este tipo de datos ofrece otra ventaja que es muy importante de cara al rendimiento: no reserva buffers intermedios y evita las copias de datos innecesarias. Deja vu! Empezamos el nuevo año como mismo terminamos el viejo: hablando de un tipo de datos por valor (estructura) que ofrece mejoras de rendimiento.

El siguiente ejemplo muestra cómo crear un Span<T> a partir de un array, cómo hacer referencia a un elemento cualquiera, y (lo mejor de todo), cómo utilizar el método Slice para denotar a un subconjunto (“rebanada”) de los elementos del span. Para finalizar, ordeno la rebanada mediante una implementación “espantosa”1 de QuickSort. Tengo la impresión de que este algoritmo se logra expresar mucho más elegantemente cuando se utiliza Span<T>.

using System;

namespace SpanDemo
{
    public static class SortingRoutines
    {
        static void Main(string[] args)
        {
            var random = new Random();

            int[] values = new int[100]; 
            for (int i = 0; i < 100; i++)
                values[i] = random.Next(0, 1000);

            var span = new Span<int>(values);
            // Referencias a elemento
            Console.WriteLine("First element: {0}", span[0]);
            span[1]++;
            // "Rebanada" del span original
            var span2 = span.Slice(start: 3, length: 10);

            QuickSort(span2);
            foreach (int i in span2)
                Console.WriteLine(i);
        }

        public static void QuickSort<T>(Span<T> span) where T: IComparable 
        {
            int i = 0, j = span.Length - 1;
            T pivot = span[(span.Length - 1) / 2];

            while (i <= j)
            {
                while (span[i].CompareTo(pivot) < 0) i++;
                while (span[j].CompareTo(pivot) > 0) j--;
                if (i <= j)
                {
                    T tmp = span[i];
                    span[i] = span[j];
                    span[j] = tmp; 

                    i++; j--;
                }
            }

            // Llamadas recursivas
            if (j > 0)
                QuickSort<T>(span.Slice(0, j + 1));
            if (i < span.Length - 1)
                QuickSort<T>(span.Slice(i, span.Length - i));
        }
    }
}

Dejo al lector la investigación de cómo se implementa internamente la estructura Span<T>, algo que describen perfectamente tanto Ahson Khan en Code Magazine como Stephen Toub en MSDN Magazine.

Para finalizar, la diatriba prometida contra la exclusión de Span<T> de .NET Framework. No es la necesidad de añadir un paquete, ni mucho menos; es la fractura que empieza a producirse en el lenguaje por culpa de ello. Por ejemplo, Microsoft señala en este artículo que los futuros índices y rangos de C# 8.0 dependen de Span<T> y otras nuevas características, y no estarán disponibles al desarrollar aplicaciones para .NET Framework. Oficialmente, nos dicen que estos nuevos tipos no se añaden a .NET Framework para no comprometer la estabilidad de la plataforma; en el fondo, lo que pienso es que lo que Microsoft quiere es obligarnos a ir hacia .NET Core. Tal vez algún lector logre aclararme esta confusión.


1 “Espantoso” no viene de espanto, sino de Span<T> .

Referencia musical: El grupo británico Yes (elegido al Salón de la Fama del Rock’n’Roll en 2017) es uno de los máximos exponentes de la corriente que, por razones obvias, se ha dado en llamar rock sinfónico o progresivo, y “Tales from Topographic Oceans” (1973), al que pertenece la referencia musical de hoy, uno de los primeros exponentes de álbum conceptual, en este caso inspirado en la “Autobiografía de un Yogi”, de Paramahansa Yogananda.

La estructura ValueTask

“… ‘Cause I’ve been here, and I’ve been there,
Seems like I’ve been everywhere before.
I’ve seen it all a hundred times
Still I think there surely must be more…”
Kansas, “Paradox” (1977)

Repasando la enorme cantidad de características añadidas a  C# en las versiones 6 y 7.x, realmente puede dar la impresión de que (como dice la referencia musical de hoy) no queda nada más que inventar. Sin embargo, ya tenemos casi a la vuelta de la esquina a C# 8.0, con una buena cantidad de novedades relevantes.

Buscando entre las características incorporadas a partir de la versión 7.0 de las que no he hablado aún aquí (y que no sean totalmente triviales, como por ejemplo las mejoras en la representación de constantes numéricas), encontré la generalización de los tipos de retorno de los métodos asíncronos (generalized async return types), una característica que ha cobrado importancia principalmente debido a la aparición de un nuevo tipo que se ha hecho posible gracias a ella, ValueTask<T>,  que hace posible mejorar el rendimiento en ciertos escenarios de utilización de llamadas asíncronas.

Como seguramente sabrá, antes de C# 7.0 los métodos asíncronos solo podían devolver los tipos Task<TResult> (si se retorna un valor), Task (si el método realiza una acción y no retorna ningún valor) o void (en el caso de los gestores de eventos asíncronos1). A partir de  C# 7.0, se permite también que un método asíncrono devuelva un objeto de cualquier tipo que ofrezca un método GetAwaiter que a su vez retorne un objeto que implemente la interfaz ICriticalNotifyCompletion (espacio System.Runtime.CompilerServices). Lo más apropiado para esta entrada sería crear precisamente un tipo así, pero tengo la impresión de que eso tendría un interés puramente académico: no he encontrado en la web ninguna referencia a un tipo similar creado por terceros. Da la impresión de que esta generalización se ha hecho con el objetivo específico de abrir las puertas a la definición de ValueTask<T>, que precisamente cumple con ese requisito y que además tiene otra característica esencial: no es un tipo por referencia (class) sino un tipo por valor (struct). Es este hecho el que hace posible una mejora del rendimiento en ciertos escenarios de uso de los métodos asíncronos, como describiremos a continuación.

Para el ejemplo de hoy utilizaré la peor manera posible (o la más ingenua, si queremos ser más políticamente correctos) de implementar el cálculo del n-simo elemento de la serie de Fibonacci, en la que cada elemento subsiguiente de la serie se obtiene sumando los dos anteriores a él. Y como siempre es posible empeorar aún más algo que ya está mal, he añadido una “guinda al pastel” haciendo la que las llamadas recursivas se realicen de manera asíncrona. Esta variante asíncrona es la que me servirá como base para mostrar cómo el uso de ValueTask mejora el rendimiento (¿le parece a usted, querido lector, que el fin siempre justifique los medios? ;-)). Eche un vistazo al siguiente código:

using System;
using System.Threading.Tasks;

namespace Fibo
{
    class MainClass
    {
        static void Main(string[] args)
        {
            TimeMethod(() => { Console.WriteLine(Fibo(40)); } );
            TimeMethod(() => { Console.WriteLine(FiboAsync(40).Result ); } );
        }

        static long Fibo(int n)
        {
            if (n <= 1)
                return 1;

            return Fibo(n - 2) + Fibo(n - 1);
        }

        static async Task<long> FiboAsync(int n)
        {
            if (n <= 1)
                return 1;

            return await FiboAsync(n - 2) + await FiboAsync(n - 1);
        }

        static void TimeMethod(Action action)
        {
            DateTime t1 = DateTime.Now;
            action();
            DateTime t2 = DateTime.Now;
            Console.WriteLine("Ellapsed " +
                (t2 - t1).TotalMilliseconds + " ms.");
        }
    }
}

La salida que produce el programa anterior es la siguiente:

165580141
Ellapsed 1612.459 ms.
165580141
Ellapsed 44734.77 ms.

Así que introduciendo la asincronía sin que hubiera necesidad alguna hemos hecho el código unas 27 veces más lento :-). Pero esta entrada no intenta predicar sobre en qué situaciones se deben utilizar los métodos asíncronos; este enlace podría ser un buen comienzo para ello. Aquí yo solo intentaba buscar un ejemplo en el que el uso de ValueTask<T> ofreciera una mejora en rendimiento sobre Task<T>, y me pareció que éste podría ser un buen caso, porque en él se realiza una gran cantidad de cálculos repetidos, y cada invocación recursiva termina provocando llamadas a los casos base, en los que el resultado (1) está predeterminado.

Para poder hacer uso de la estructura ValueTask<T>, hace falta importar de Nuget el paquete System.Threading.Tasks.Extensions:

Ahora, sustituyendo la única referencia a Task en el código fuente por ValueTask y ejecutando, se obtiene como promedio lo siguiente:

165580141
Ellapsed 1617.608 ms.
165580141
Ellapsed 39123.887 ms.

La ganancia no es tanta como yo esperaba, pero en cualquier caso un 13% de mejora en el rendimiento no es nada despreciable.

La razón por la que el rendimiento es superior al utilizar ValueTask estriba en que cuando se utiliza el tipo Task, que es una clase, es necesario cumplir con el protocolo de instanciación de un objeto incluso en los casos base, para los que el resultado está predeterminado de antemano y en principio no haría falta ninguna tarea (retornando la llamada de forma síncrona). Al ser ValueTask un tipo por valor, la tarea a devolver se aloja en la pila, evitando así la costosa instanciación en todos esos casos base.

Note que también el uso de ValueTask hace posible una menor presión sobre la memoria (memory pressure), y una disminución en la frecuencia entre ejecuciones del recolector de basura (garbage collector). Este vídeo de Andrea Angella ilustra fenomenalmente este beneficio.


1 Frecuentemente encontrará recomendaciones sobre no crear métodos async void, como ésta de Bill Wagner.

Referencia musical: “Paradox” pertenece al “Point of Know Return” (1977), que es mi disco favorito de mi banda favorita, Kansas, a pesar de que la critica generalmente considera superior al proyecto anterior, “Leftoverture” (1976) – vea por ejemplo este reportaje de Rolling Stone que lo coloca en el lugar 32 de los mejores álbumes de rock progresivo de todos los tiempos.

Contando palabras reservadas

Intentando continuar la serie dedicada a las novedades aparecidas en C# 7.0 y versiones posteriores, se me ocurrió escribir un programa que contara las apariciones de las diferentes palabras reservadas (keywords) y palabras reservadas contextuales (contextual keywords) de C# en un fichero de código fuente individual o un conjunto de ficheros de código fuente alojados en una estructura de carpetas anidadas. Como lejana fuente de inspiración me sirvió el recuerdo de un programa para contar las palabras de un fichero incluido en un libro que ha dejado huella, “The C Programming Language“, de Kernighan y Ritchie.

Originalmente pensé que podría utilizar el programa para mostrar las ventajas que puede aportar al rendimiento la utilización de las variables locales y valores de retorno por referencia (ref locals and returns) incorporadas a C# 7.0. Lo cierto es que a lo largo del camino perdí el norte (tal vez me reencuentre con él en una próxima entrega), pero pienso que el viaje ha valido la pena; en particular, por fin he hecho uso (aunque de una manera trivial, lo reconozco) de las posibilidades que ofrece .NET Compiler Platform, la tecnología anteriormente conocida como Roslyn. Espero que el lector también saque algo positivo de la lectura de esta entrada, y para contribuir más a ello pongo a su disposición el código del proyecto, en el que también se utilizan otras novedades de C# 7.0 como las tuplas-valor o las funciones anidadas.

No me extenderé mucho aquí en una introducción al uso de .NET Compiler Platform, porque el lector encontrará múltiples tutoriales de calidad en la web, en particular para el análisis sintáctico; un buen ejemplo es éste. Baste decir que solo es necesario añadir a su proyecto el paquete de NuGet Microsoft.CodeAnalysis.CSharp, y ello le dará acceso a toda la gama de herramientas para la compilación que ofrece Roslyn. Gracias a esa potencia, el método central de nuestro ejemplo, que procesa un fichero de código fuente C#, consiste en unas pocas líneas:

private static void ProcessFile(string fileName)
{
    var tree = CSharpSyntaxTree.ParseText(File.ReadAllText(fileName));
    Traverse(tree.GetRoot());

    void Traverse(SyntaxNode node)
    {
        foreach (var childToken in node.ChildTokens())
        {
            if (childToken.IsKeyword() || childToken.IsContextualKeyword())
            {
                var text = childToken.Text;
                dict[text] = (dict[text].IsContextual, dict[text].Count + 1);
            }
        }

        foreach (var childNode in node.ChildNodes())
            Traverse(childNode);
    }
}

Las listas completas de palabras reservadas y palabras reservadas contextuales de C# las tomé directamente del código fuente de Roslyn en Github; las que intenté tomar de otras fuentes estaban siempre incompletas, como pude comprobar al ejecutar el programa sobre un conjunto de ficheros de la vida real. Las cinco palabras reservadas más utilizadas en ese conjunto son, en orden descendente: using, public, new, return y private.

El futuro atributo CallerArgumentExpression: un deseo que se cumplirá

“… It’s too bad that all these things
Can only happen in my dreams
Only in dreams, in beautiful dreams …”
Roy Orbison, “In Dreams” (1963)

“… We all know / That people are the same wherever you go …”
Paul McCartney & Stevie Wonder, “Ebony and Ivory” (1982)

Hace unos cuantos meses, mi buen amigo Eduard Tomàs publicó en este mismo sitio una entrada con título auto-descriptivo llamada “Hoy he echado en falta poder definir macros en C#“. En la discusión subsiguiente, especulamos sobre un hipotético atributo de .NET que hiciera posible satisfacer su deseo incumplido. Pues bien, ayer revisando las propuestas de características a añadir en las próximas versiones del lenguaje (en el sitio de Github en el que el equipo de C# publica las minutas de sus reuniones, entre otros documentos muy interesantes) me encontré con la descripción del futuro atributo CallerArgumentExpression, que (en el supuesto de que sea implementado, claro), permitirá lograr fácilmente el efecto que Eduard quería obtener aquella vez.

La propuesta de esta nueva característica está siendo considerada por el equipo de C# desde mayo de 2017, unos cuantos meses antes de que nosotros conversáramos sobre el tema; ello me reafirma una vez más en que, como dijeran magistralmente Paul y Stevie, la gente (y en particular, los programadores) tiene las mismas necesidades en todos los lugares. 🙂

using System.Runtime.CompilerServices;

public static class Check
{
    public static void NotNull<TParameter>(TParameter param, 
        [CallerArgumentExpression("param")] string expr = "")
        where TParameter : class
    {
        if (param == null)
        {
            throw new ArgumentNullException(paramName: expr);
        }
    }
}

Referencia musical: Intenté infructuosamente encontrar alguna canción de mi época que dijera que algunos sueños a veces se cumplen; no encontré ninguna, así que he puesto otra que dice más o menos lo contrario. Su autor fue el genial Roy Orbison, a quien mayormente se le recuerda por haber escrito la canción que sirvió de tema a la película “Pretty Woman“. De Paul McCartney y Stevie Wonder no diré nada más aquí – son dos de mis músicos favoritos y seguramente los he mencionado ya en alguna entrada anterior.

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.