Variables locales y valores de retorno por referencia

“… Turn around/Turn around
It’s on the other side
Feel the sound/Feel the sound
It’s coming from deep inside…”
Kansas, “On the Other Side” (1979)

La entrada anterior tenía al principio como objetivo mostrar el uso de las variables locales y valores de retorno por referencia (ref locals and returns), pero a lo largo del camino me fui desviando de ese objetivo inicial, para terminar hablando de otra cosa totalmente diferente. Con esta entrada intento retomar la senda perdida aquella vez y mostrar la utilización de estas novedosas posibilidades que ofrece C#, aparecidas por primera vez en la versión 7.0.

Estas características tienen como objetivo principal el de introducir una semántica de punteros sin que sea necesario recurrir al código no seguro (unsafe code), con la finalidad última de promover ventajas de rendimiento, de manera similar a los parámetros ref de toda la vida. Por ejemplo, suponiendo que dispusiéramos de una estructura de gran tamaño y necesitáramos pasarla a un método como parámetro, la posibilidad de pasar una referencia a la estructura en lugar de la estructura en sí nos permitiría ahorrar tanto en espacio de pila como en el tiempo necesario para la copia. Aquí me he montado un ejemplo rápido, aunque probablemente haría falta una estructura bastante más grande para que la ganancia fuera perceptible:

namespace RefReturnsAndLocals
{
    public struct Point
    {
        public int X, Y, Z;
        public override string ToString() => $"({X}, {Y}, {Z})";
    }

    static class MainClass
    {
        static void Main()
        {
            Point p = new Point { X = 0, Y = 0, Z = 0 };
            Drift(ref p, 100);
            Console.WriteLine(p);
        }

        static void Drift(ref Point point, int steps)
        {
            var rnd = new Random();
            for (int i = 0; i < steps; i++)
            {
                point.X += rnd.Next(-5, 6);
                point.Y += rnd.Next(-5, 6);
                point.Z += rnd.Next(-5, 6);
            }
        }
    }
}

Las referencias locales operan de manera bastante similar a los parámetros ref; por ejemplo, en el método Drift podríamos haber introducido una variable-referencia local, inicializarla para apuntar al mismo sitio al que apunta el parámetro de entrada y utilizarla en los cálculos:

        static void Drift(ref Point point, int steps)
        {
            ref Point p = ref point;
            var rnd = new Random();
            for (int i = 0; i < steps; i++)
            {
                p.X += rnd.Next(-5, 6);
                p.Y += rnd.Next(-5, 6);
                p.Z += rnd.Next(-5, 6);
            }
        }
    }
}

Observe cómo hay que utilizar ref tanto en la declaración de la variable como en la expresión de inicialización. Al declarar una referencia local como ésta, siempre se la debe inicializar; a partir de C# 7.3 (¿se había Ud. dado cuenta de que ya está disponible?), es posible poner una referencia local a “apuntar” a otro objeto del tipo adecuado en cualquier momento posterior a la inicialización.

Dejaré la presentación de los valores de retorno por referencia, que tienen sus propios detalles interesantes, para la próxima entrega. Pero antes de finalizar aquí, vale la pena recordar una vez más que siempre que se trabaja con punteros, los peligros acechan en todo momento. Por ejemplo, quite el prefijo ref en la declaración de esta última versión del método Drift y en la correspondiente llamada; obtendrá un programa que compila, pero no funciona (¿por qué, amigo lector?).


Referencia musical: “On the Other Side” es el primer tema del “Monolith” (1979), el primer disco que mi banda favorita, Kansas, produjo de manera independiente, un derecho que se ganó a pulso después del enorme éxito de sus proyectos anteriores, “Leftoverture” (1976), “Point of Know Return” (1977) y “Two for the Show” (directo, 1978). Si bien ese disco no alcanzó el nivel de los anteriores, todavía tuvo un relativo éxito; algunos de sus temas aún se dejan oír después de casi 40 años, especialmente “On the Other Side” y “People of the South Wind“, dedicada a la tribu aborigen de la que se derivó originalmente el nombre Kansas.

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.