C#: Los peligros de las variables estáticas en librerías

Estamos acostumbrados a usar variables estáticas para valores “constantes”, que no cambiarán de valor independientemente del estado de la aplicación. El ejemplo más típico es el valor de PI. Lo cierto es que suena tan sencillo que rara vez nos paramos a pensar si estamos usando las variables estáticas donde debemos o no. Sin embargo, esto puede traer bastantes quebraderos de cabeza si estamos creando código que será usado por otros…

Supongamos que hemos desarrollado una librería que se encarga de gestionar conexiones para algún servicio (IncredibleService) que tiene un número máximo de llamadas a la API por hora, 150. Dado que ese límite es constante y no cambiará independientemente del estado de la aplicación, podríamos escribir algo así:

Esa librería podría ser usada por un programa como este:

1
2
3
4
5
6
7
using System;
 
public class IncredibleService
{
        public const int MaxApiCalls = 150;
        // Here we'd have the rest of the implementation
}

Esa librería podría ser usada por un programa como este:

1
2
3
4
5
6
7
8
9
using System;
 
public class Program
{
        public static void Main()
        {
                Console.WriteLine("Maximum number of calls to the API: " + IncredibleService.MaxApiCalls);
        }
}

Si compilamos la librería, compilamos el programa y lo ejecutamos, nos mostraría por pantalla el texto “Maximum number of calls to the API: 150”, que es precisamente lo que debería pasar, así que hasta aquí, todo bien.

Pablos-MacBook-Pro:csharp carballude$ dmcs -target:library IncredibleService.cs
Pablos-MacBook-Pro:csharp carballude$ dmcs -reference:IncredibleService.dll Program.cs
Pablos-MacBook-Pro:csharp carballude$ mono Program.exe
Maximum number of calls to the API: 150

Supongamos que tres meses más tarde, el numero de usuarios del servicio crece y la empresa decide realizar una inversión y mejorar la experiencia de los usuarios permitiendo el doble de consultas a la API por hora, es decir, 300. Nosotros, como desarrolladores de la librería IncredibleService deberíamos cambiar el valor 150 por 300, compilar la librería y distribuirla a nuestros clientes:

1
2
3
4
5
6
7
using System;
 
public class IncredibleService
{
        public const int MaxApiCalls = 300;
        // Here we'd have the rest of the implementation
}

La mayoría de nosotros esperaríamos que una vez que Program.exe haya recibido la nueva dll, mostraría por pantalla el valor 300… sin embargo:

Pablos-MacBook-Pro:csharp carballude$ dmcs -target:library IncredibleService.cs
Pablos-MacBook-Pro:csharp carballude$ mono Program.exe
Maximum number of calls to the API: 150

¿Cómo es posible que IncredibleService.dll tenga el valor 300 para MaxApiCalls, Program.exe llame a IncredibleService.MaxApiCalls y este siga mostrando el valor 150? Yo también me quedé con un palmo la primera vez que lo vi xD Examinemos lo que ha generado el compilador:

Pablos-MacBook-Pro:csharp carballude$ mono disassembler.exe --disassemble-all --methods-matching Main Program.exe
DotNet disassembler v1.0 - Pablo Carballude
Program
	Public Void Main()
		-- Local variables
		-- End of local variables
		IL_0: ldstr
		IL_1: break
		IL_2: nop
		IL_3: nop
		IL_4: cpobj
		IL_5: ldc.i4
		IL_6: ldelem.i8
		IL_7: nop
		IL_8: nop
		IL_9: nop
		IL_10: box
		IL_11: ldarg.0
		IL_12: nop
		IL_13: nop
		IL_14: break
		IL_15: call System.String::Concat
		IL_20: call System.Console::WriteLine
		IL_25: ret

Si observamos el código, veremos que el compilador ha generado “ldc.i4” en el IL_5, es decir, está guardando el valor directamente en nuestro código y no mirando la librería. ¿Por qué? Fácil, el compilador se ha dado cuenta de que estamos llamando a IncredibleService para leer una constante, dado que esa constante no va a cambiar de valor (porque para eso es constante) en vez de cargar la dll en memoria y hacer la llamada, ahorra memoria y tiempo incrustando el valor en nuestra código directamente. El problema es que si actualizamos el valor de la constante en la dll, el programa no recibirá el nuevo valor hasta que la recompilemos contra la nueva versión ¡porque la librería ni siquiera se está cargando en memoria!

Arreglando el entuerto

Cuando tengamos que fijar un valor que va a ser constante respecto del estado de la aplicación, pero susceptible de ser modificado (como el caso del ejemplo) es aconsejable hacer uso de variables “readonly”. El código quedaría así:

1
2
3
4
5
6
7
using System;
 
public class IncredibleService
{
        public static readonly int MaxApiCalls = 150;
        // Here we'd have the rest of the implementation
}

Ahora la variable no es “const” sino “static readonly”, pero esto no produce ningún cambio sintáctico en los clientes, así que podemos compilar Program sin hacerle ningún cambio:

Pablos-MacBook-Pro:csharp carballude$ dmcs -target:library IncredibleService.cs
Pablos-MacBook-Pro:csharp carballude$ dmcs -reference:IncredibleService.dll Program.cs
Pablos-MacBook-Pro:csharp carballude$ mono Program.exe
Maximum number of calls to the API: 150

Si ahora actualizamos el código de IncredibleService.dll para que tenga el valor 300 y compilamos, Program sí mostrará el resultado esperado:

Pablos-MacBook-Pro:csharp carballude$ dmcs -target:library IncredibleService.cs
Pablos-MacBook-Pro:csharp carballude$ mono Program.exe
Maximum number of calls to the API: 300

Podemos comprobar además que ahora el código generado por el compilador sí llama a nuestra dll:

Pablos-MacBook-Pro:csharp carballude$ mono disassembler.exe --disassemble-all --methods-matching Main Program.exe
DotNet disassembler v1.0 - Pablo Carballude
Program
	Public Void Main()
		-- Local variables
		-- End of local variables
		IL_0: ldstr
		IL_1: break
		IL_2: nop
		IL_3: nop
		IL_4: cpobj
		IL_5: ldsfld
		IL_6: ldarg.0
		IL_7: nop
		IL_8: nop
		IL_9: stloc.0
		IL_10: box
		IL_11: ldarg.1
		IL_12: nop
		IL_13: nop
		IL_14: break
		IL_15: call System.String::Concat
		IL_20: call System.Console::WriteLine
		IL_25: ret

Ahora el compilador sí ha generado código para mirar el campo estático «ldsfld» y no está incrustándolo en nuestra aplicación :)

About the Author

Me llamo Pablo Carballude González, soy graduado en computación con master en HCI y Seguridad Informática. Actualmente trabajo para Amazon en Seattle como Software Developer Engineer. Soy de esas personas que no saben si los textos autobiográficos deben ser en primera o tercera persona. Lo intenté en segunda, pero no le entendí nada :P