C# 8.0: Funciones locales estáticas

En la entrada anterior comenzamos a hablar sobre las nuevas características que se añadirán a C# 8.0, partiendo de las más simples, en aquella ocasión las declaraciones using (using declarations). Hoy nos centraremos en otra al parecer muy sencilla, las funciones locales estáticas (static local functions). En este caso, la impresión que tengo es que el equipo de diseño del lenguaje no hizo caso a la cita de Einstein que mencionamos la vez anterior y se quedó un poco “corto” en la oferta; o tal vez será que habrán decidido dejar algo para la futura versión 9 :-).

En el artículo que dedicamos a las funciones locales hace ya más de año y medio mencionábamos las ventajas de la anidación de funciones. Para mí (tal vez por provenir del mundo de Pascal, donde tal posibilidad siempre existió), la principal radica en que permiten acotar el ámbito de utilización del identificador correspondiente, y con ello promueven un mayor encapsulamiento del código. Todo lo demás es, simplemente, secundario.

Al hacer estática una función local en C# 8.0, el programador le estaría diciendo al compilador que dicha función no depende en lo absoluto del contexto que la contiene; o sea, que se trata de una función “pura” que se comunica con dicho contexto exclusivamente a través de los parámetros de entrada y el valor de retorno. Un ejemplo sencillo podría ser el siguiente, donde se calcula la raíz cuadrada de un número utilizando el método de aproximaciones sucesivas de Newton:

static double SquareRoot(double x, double epsilon)
{
    Debug.Assert(x >= 0);
    Debug.Assert(epsilon > 0 && epsilon < 0.1);

    double y = 1.0;
    while (true)
    {
        double y1 = Average(y, x / y);
        if (Math.Abs(y1 - y) < epsilon)
            break;
        y = y1;
    }
    return y;

    static double Average(double a, double b) => 0.5 * (a + b);
}

La restricción de no acceder a los elementos del contexto que las rodea limita hasta cierto punto las posibles aplicaciones de este tipo de funciones, en particular a la hora de capturar las variables locales del método que las contiene. Por ejemplo, la función interna que utilizamos en el artículo anterior no podría ser marcada como estática. No obstante, la adición de esta nueva posibilidad es sin duda positiva, en particular porque permite al programador indicar al compilador de manera explícita sus intenciones, y que éste verifique que realmente la función no se excede de los límites que se han establecido para ella. De paso, se hacen posibles ciertas pequeñas mejoras en el rendimiento, al no ser necesario gestionar el mecanismo de capturas, como puede leerse aquí.

Cuando mencionaba al principio que pensaba que la oferta se había quedado corta, estaba pensando específicamente en otra característica que creo que complementaría a ésta en gran medida: las variables locales estáticas. Como bien conocen aquellos que utilizan C, C++ o incluso Visual Basic, una variable local estática es una variable local que conserva su valor de una llamada a la función a otra; o sea, que es local en lo que a ámbito se refiere, pero global por su tiempo de vida. Las razones por las cuales esta característica nunca ha sido añadida C# (por ejemplo, las que se enumeran aquí) nunca me convencieron del todo. De nuevo, lo principal para mí es la acotación al máximo del ámbito; no me vale el argumento de que “eso se puede lograr añadiendo un campo estático a la clase”.

El ejemplo de uso por excelencia de las variables locales estáticas para mí siempre será un generador de números pseudo-aleatorios. En tales funciones, cada nuevo valor se computa utilizando una fórmula compleja (que muy frecuentemente involucra números primos) a partir del valor anterior; o sea, que la variable que almacena el último valor generado deberá conservar su valor para la próxima llamada, en la que servirá como argumento. Existen múltiples algoritmos de generación de tales secuencias; aquí utilizaremos el Algoritmo Linear Congruente que describió James McCaffrey en su artículo “Lightweight Random Number Generation” de MSDN Magazine (agosto de 2016).

La clase que allí se presenta para implementar el generador es la siguiente:

public class LinearConRng
{
  private const long a = 25214903917;
  private const long c = 11;
  private long seed;
  public LinearConRng(long seed)
  {
    if (seed < 0)
      throw new Exception("Bad seed");
    this.seed = seed;
  }
  private int next(int bits) // helper
  {
    seed = (seed * a + c) & ((1L << 48) - 1);
    return (int)(seed >> (48 - bits));
  }
  public double Next()
  {
    return (((long)next(26) << 27) + next(27)) / (double)(1L << 53);
  }
}

En el código anterior, la variable seed (“semilla”), que es la que se actualiza en cada iteración, ha sido promovida a variable de instancia con el ¿aceptable? pretexto de poder ser inicializada desde fuera a través del parámetro del constructor. Lo cierto es que en la práctica, en este tipo de algoritmos la semilla se calcula utilizando, por ejemplo, la hora actual del reloj del ordenador, y ese cálculo se podría hacer perfectamente como parte de la inicialización de una variable local estática (en caso de que existieran). No faltará aquí quien ponga el grito en el cielo pensando en que esta inicialización podría complicarse en contextos complejos, de múltiples hilos, etc. Mi respuesta: nada con lo que un compilador inteligente apoyado en el runtime de .NET no pueda lidiar con éxito.

Si existieran las variables locales estáticas, la clase anterior podría modificarse como sigue:

public static class LinearConRng
{
  // *** WARNING: not valid C# 8.0 ***
  public static double Next()
  {
    private static long seed = DateTime.Now.Ticks; // !!!

    private static int next(int bits) // helper
    {
      const long a = 25214903917;
      const long c = 11;

      seed = (seed * a + c) & ((1L << 48) - 1);
      return (int)(seed >> (48 - bits));
    }

    return (((long)next(26) << 27) + next(27)) / (double)(1L << 53);
  }
}

Un detalle que me llamó la atención mientras estaba jugando con el código es que incluso referencias externas a constantes como a y c desde dentro de una función local estática producen el error CS8421: A static local function cannot contain a reference to ‘a’. Eso parece indicar que estas entidades se tratan como variables locales (a las que el compilador no permite hacer asignaciones después de su inicialización) más que como verdaderas constantes. Tengo que comprobar qué dice la especificación de C# al respecto.

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *