Referencias anulables en C# 8.0: ¿presente o ausente?

“I know you’re here but you’re not really there…”
Kansas, “The Absence of Presence” (2020)

A lo largo de múltiples entradas anteriores hemos venido describiendo las nuevas características que se incluyeron en la versión 8.0 del lenguaje C#, y creo que terminamos a tiempo, ahora que se ha anunciado ya C# 9.0 y se han empezado a desvelar algunas de las futuras posibilidades que ofrecerá (vea, por ejemplo, este enlace). En lo fundamental, solo nos ha faltado hablar de una característica aparecida con C# 8.0, pero que probablemente haya sido la más importante de todas: los tipos por referencia anulables (nullable reference types). He evitado hasta el momento hablar de ellos hasta ahora porque, honestamente, no he tenido la oportunidad de utilizarlas en la práctica por limitaciones de los proyectos en los que he trabajado. En cualquier caso, es importante notar que esta novedosa característica eleva el lenguaje, en mi modesta opinión, a un nuevo nivel, y que incorporarla sobre la marcha a proyectos y existentes requiere, se me antoja, una cierta consideración. En particular, los siguientes enlaces a documentación oficial de Microsoft pueden servirle como guía para ello: Migrate existing code with nullable reference typesChoose a strategy for enabling nullable reference types e Incorporate nullable reference types into your designs.

Los tipos por referencia anulables de C# 8.0 tienen como objetivo superar, de una manera declarativa, una de las principales fuentes de errores de ejecución en los sistemas de programación modernos: las referencias o punteros nulos, que se producen cuando no se ha asignado valor a una referencia o puntero que debe estar «apuntando» a algún lado antes de ser utilizada. Ya dijo el venerable Tony Hoare que inventar las referencias nulas en ALGOL-W (por allá por 1965) fue su «billion dollar mistake» (vea, por ejemplo, este vídeo).

Como el uso de los tipos por referencia anulables puede producir rupturas con la compatibilidad hacia atrás del código, por defecto están desactivados. Su utilización se deberá activar explícitamente utilizando opciones o directivas de de compilación, creando lo que se conoce oficialmente como contextos de anulabilidad (nullability contexts). Una vez activa, esta nueva característica del lenguaje permite al programador declarar explícitamente sus intenciones con relación a las variables de tipos por referencia usando la misma notación que ya se utiliza hoy para los tipos por valor anulables: añadiendo un signo de interrogación (?) detrás del nombre del tipo. Por ejemplo, para indicar que una variable de tipo string puede tener el valor null (en un contexto en el que esté activa la compilación con tipos por referencia anulables), se declarará la variable de esta manera:

    string? middleName;  // opcional en USA

En tales contextos, la declaración habitual un tipo por referencia sirve para indicar que la variable o campo no puede nunca tomar el valor null:

    string firstName;  // obligatorio en USA

Cuando se activa la compilación con tipos por referencia anulables, el comportamiento del compilador se modifica con respecto al que ha sido común hasta la versión 7.3 de C# para aceptar la notación antes mencionada y dar soporte a los siguientes dos escenarios:

  1. Si una variable de un tipo por referencia nunca debe tener el valor null, el compilador impondrá reglas de compilación que garanticen que será seguro acceder al objeto al que hace referencia la variable sin necesidad de comprobar antes que la referencia no es nula:
    • La variable deberá ser inicializada con un valor no nulo.
    • A la variable nunca se le deberá asignar el valor null.
  2. Si una variable de un tipo por referencia puede tener el valor null, el compilador utilizará un conjunto de reglas de compilación diferentes para asegurarse de que se haya comprobado que la referencia no es nula antes de acceder al contenido referenciado:
    • Solo se puede acceder al objeto apuntado por la variable cuando se puede garantizar que la referencia no es nula.
    • A estas variables se las puede inicializar con el valor null y se les puede asignar el valor null en otras partes del código.

Un ejemplo básico

Presentaremos los aspectos básicos relacionados con los tipos por referencia anulables utilizando un ejemplo muy sencillo. Para comenzar, crearemos un proyecto de aplicación de consola con C# 8.0, e inmediatamente editaremos el archivo de proyecto, NullableReferenceTypes.csproj, para activar los tipos por referencia anulables:

    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    <!-- ... -->
    </Project>

A continuación, editaremos el fichero de código fuente, Program.cs, para que quede de la siguiente forma:

    01  class Program
    02  {
    03      static void Main(string[] args)
    04      {
    05          Greeter myGreeter = null;
    06          myGreeter.Salute("Octavio");
    07      }
    08  }
    09
    10  class Greeter
    11  {
    12      public void Salute(string name)
    13      {
    14          System.Console.WriteLine("Hello, " + name);
    15      }
    16  }

Sin necesidad de compilar, notará que el código anterior tiene dos fallos monumentales, que en C# 7 y anteriores quedarían para ser depurados en ejecución. El compilador de C# 8.0, sin embargo, al ver que la variable myGreeter es de un tipo no anulable (pues no hay signo de interrogación en la declaración, así que estamos en presencia del escenario número 1), nos reportará las dos siguientes claras advertencias. Son solo advertencias; sin embargo, si es Usted como yo, añadirá la línea <TreatWarningsAsErrors>true</TreatWarningsAsErrors> al fichero de proyecto para que todas las advertencias (y éstas en particular) se traten como errores:

    CS 8600: Converting null literal or possible null value to non-nullable type [line 05]
    CS 8602: Dereference of a possibly null reference [line 06]

¿Y qué pasa si modificamos la línea 05 para indicar que myGreeter acepta el valor nulo (escenario número 2)?

    05          Greeter? myGreeter = null;

Pues en este caso aún verá la segunda advertencia (o error, según la configuración del proyecto):

    CS 8602: Dereference of a possibly null reference [line 06]

Aquí el compilador nos advierte de que podría producirse una excepción de referencia nula, porque el analizador de flujo no ve que hayamos comprobado que el valor de la variable myGreeter no es nulo antes de utilizarla. El lector puede comprobar que la advertencia desaparecerá si se añade la condicional correspondiente:

    06      if (myGreeter != null) myGreeter.Salute("Octavio");

Para completar nuestra presentación de hoy, a continuación presentamos una propuesta de cómo «arreglar» el proyecto afirmando no solo la no anulabilidad de la variable myGreeter, sino también la anulabilidad del parámetro name del método SayHello:

    01  class Program
    02  {
    03      static void Main(string[] args)
    04      {
    05          Greeter myGreeter = new Greeter();
    06          myGreeter.Salute("Octavio");
    07      }
    08  }
    09
    10  class Greeter
    11  {
    12      public void Salute(string? name)
    13      {
    14          if (string.IsNullOrWhiteSpace(name))
    15          {
    16              System.Console.WriteLine("Hello, " + name);
    17          } 
    18          else
    19          {
    20              System.Console.WriteLine("Hello, " + name);
    21          }
    22      } 
    23  }

Por supuesto, se trata de un ejemplo trivial que solo roza la superficie de este tema; los tipos por referencia anulables dan para mucho más. En próximas entradas intentaremos profundizar en el contenido de esta entrada y complementarlo.


Referencia musical: Mi banda favorita, Kansas, acaba de añadir a su ya venerable discografía una nueva entrada, «The Absence of Presence» (2020), que recomiendo a todos los amantes del rock clásico y el rock sinfónico. La letra de la canción que da nombre al disco alude a una triste característica de los tiempos que corren: cómo alguien puede estar presente de cuerpo en un lugar, pero mentalmente ausente, concentrado únicamente en lo que emerge de la pantalla de su teléfono móvil. Algo hasta cierto punto parecido a lo que puede pasar con las referencias anulables: uno accede a la variable, pero ésta no apunta a ningún lado.

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 *