El modificador readonly en estructuras

Como comentaba en una entrada anterior, 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. Hoy hablaremos sobre la utilización del modificador readonly al definir estructuras (structs), que como bien conoce el lector son tipos que se pasan por valor en la pila cuando se utilizan como parámetros de métodos. También veremos cómo C# 8.0 da una pequeña “vuelta de tuerca” adicional a esta característica.

La versión 7.2 de C# añadió la posibilidad de aplicar el modificador readonly a una estructura (como un todo) para indicar que el tipo en cuestión es inmutable, lo que significa que el estado de los objetos de ese tipo no puede modificarse después de su construcción. Las cadenas de caracteres (strings) son el ejemplo por excelencia de tipo de datos inmutable en .NET. Diseñar tipos inmutables aporta múltiples ventajas, entre las cuales podemos mencionar la seguridad añadida que se obtiene en aplicaciones con múltiples hilos gracias a que se hace posible evitar los problemas comunes de sincronización que se presentan cuando se lee y escribe de manera simultánea en una misma localización de memoria. Pero el uso de estructuras de datos inmutables también habilita varios escenarios en los que se mejora el rendimiento, como el que mencionábamos en la entrada anterior ya mencionada: cuando se utiliza el modificador in para pasar como parámetro una estructura inmutable, el compilador se puede dar el lujo de pasar una referencia al argumento correspondiente en lugar del valor, con la consiguiente economía de espacio en la pila y tiempo de ejecución, proporcional al tamaño de la estructura.

Un diseño inmutable de un tipo de datos para representar números complejos podría ser como el que se muestra a continuación (donde solo hemos implementado, por concisión, las operaciones de suma y magnitud). Este enfoque es, de hecho, el que se ha utilizado en la implementación de la estructura System.Numeric.Complex (presente tanto en .NET Framework como en .NET Core); solo que allí las propiedades X e Y se nombran Real e Imaginary.

public readonly struct Complex
{ 
    public double X { get; } 
    public double Y { get; } 

    public Complex(double x, double y) { X = x; Y = y; } 

    public double Magnitude => Math.Sqrt(X * X + Y * Y);
 
    public static Complex operator +(in Complex a, in Complex b) => 
        new Complex(a.X + b.X, a.Y + b.Y); 

    public override string ToString() => 
        $"({X}, {Y}), magnitude = {Magnitude}"; 
}

Pero no siempre las estructuras se diseñan para ser inmutables. Veamos el mismo ejemplo anterior, pero con un diseño no inmutable de la estructura Complex. Aquí ya se utiliza la nueva posibilidad que ofrece C# 8.0:

public struct Complex
{ 
    public double X { get; set; } 
    public double Y { get; set; } 

    public Complex(double x, double y) { X = x; Y = y; } 

    public readonly double Magnitude => Math.Sqrt(X * X + Y * Y);
 
    public override readonly string ToString() => 
        $"({X}, {Y}), magnitude = {Magnitude}"; 
}

La novedad consiste en que ahora es posible aplicar el modificador readonly a métodos individuales (incluyendo getters y setters de propiedades) de una estructura para indicarle al compilador que el método en cuestión no modifica el estado de la misma. El compilador nunca analiza si los métodos modifican o no el estado de las instancias de un tipo, tarea que podría resultar bastante compleja en el caso general. Así que queda de nuestra parte indicárselo. Con ello, además de la ganancia expresiva, podríamos obtener mejoras en el rendimiento en aquellos casos en que el compilador se pudiera ver obligado a crear lo que se conoce como una copia defensiva (defensive copy) de la estructura antes de hacer una llamada a método. Como ejemplo, elimine el modificador readonly de la propiedad Magnitude (pero no el del método ToString) en el código anterior. Al compilar, obtendrá la siguiente advertencia para ToString:

CS8656: Call to non-readonly member ‘Complex.Magnitude.get’ from a ‘readonly’ member results in an implicit copy of ‘this’.

Para asegurar que ToString sea readonly, el compilador, como no tiene constancia explícita de que Magnitude no modifique la estructura, deberá hacer una copia de this antes de pasarla a la llamada de modo que, en caso de que esa modificación ocurra, se produzca sobre la copia y no sobre la estructura original. Si usted descompila el código a IL, podrá ver claramente tales copias, que se implementan utilizando la instrucción ldobj. Observe que esta advertencia no aplica a los getters de propiedades implementadas automáticamente; si no, habrían tres advertencias en lugar de una. El compilador sí sabe a ciencia cierta que esos getters son inofensivos :-).

Sin duda alguna, la posibilidad de aplicar readonly a los métodos de una estructura es otra pequeña adición positiva al lenguaje, que podría llegar a ser bastante útil en escenarios de alto rendimiento.


Si el lector quiere entretenerse un poco más lidiando con las estructuras, aquí le dejo otra que he creado apoyándome en Complex: una estructura que aglutina los parámetros y las raíces de una ecuación de segundo grado. Para mí siempre es un ejercicio útil recordar fundamentos matemáticos que hace tiempo no utilizo. Todo es válido a la hora de luchar contra el tirano Alzheimer :-).

Por supuesto, se acepta todo tipo de críticas y opiniones.

public struct QuadraticEquation
{
   private double _a, _b, _c;
   private Complex _root1, _root2;

   public QuadraticEquation(double a, double b, double c)
   {
      _a = a; _b = b; _c = c;
      Solve(_a, _b, _c, out _root1, out _root2);
   }

   public double A { get { return _a; } set { _a = value; Solve(); } }
   public double B { get { return _b; } set { _b = value; Solve(); } }
   public double C { get { return _c; } set { _c = value; Solve(); } }

   public Complex Root1 { get { return _root1; } }
   public Complex Root2 { get { return _root2; } }

   private void Solve()
   {
      Solve(_a, _b, _c, out _root1, out _root2);
   }
   private static void Solve(double a, double b, double c, 
      out Complex root1, out Complex root2)
   {
      double d = b * b - 4.0 * a * c;
      if (d >= 0)
      {
         root1 = new Complex((-b + Math.Sqrt(d)) / (2.0 * a), 0);
         root2 = new Complex((-b - Math.Sqrt(d)) / (2.0 * a), 0);
      }
      else
      {
         root1 = new Complex(-b / (2.0 * a), Math.Sqrt(-d) / (2.0 * a));
         root2 = new Complex(-b / (2.0 * a), Math.Sqrt(-d) / (2.0 * a));
      }
   }
}

class Program
{
   static void Main(string[] args)
   {
      // x^2 + 2x + 2, with roots (1, 1) and (1, -1)
      var equation = new QuadraticEquation(1, 2, 2);
      Console.WriteLine($"Root1 = {equation.Root1}");
      Console.WriteLine($"Root2 = {equation.Root2}");
   }
}

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 *