Referencias anulables en C# 8.0 y posteriores (y V): atributos de anulabilidad

“I was going nowhere fast,
I was needing something that would last”
Kansas, “Paradox” (1977)

Vuelvo a la carga después de otra larga pausa para terminar la serie sobre las referencias anulables, que se ha extendido en el tiempo mucho más de lo que pensaba originalmente. Parece que voy a tener que darme prisa: ya se prepara la salida (conjuntamente con .NET 6.0 y Visual Studio 2022) de la versión 10 del lenguaje, ¡y yo todavía hablando de cosas que aparecieron en la versión 8! Aunque, como dice la referencia musical de arriba, prisa no tengo mucha, y sí me gustaría publicar algo que pueda servir como guía (y de lo que no me avergüence) dentro de unos cuantos años.

El código de ejemplo inicial en que me basaré hoy es bastante sencillo:

namespace CS90Lib
{
    using System.Diagnostics;
    using System.Linq;
    
    public static class StringFunctions
    {
        public static int GetLetterCount(string? source)
        {
            if (string.IsNullOrWhiteSpace(source))
            {
                return 0;
            }
            Debug.Assert(source != null && source.Length > 0);
            return source.Count(ch => char.IsLetter(ch));
        }
    }
}

Se trata de una pequeña librería de clases que ofrece al mundo una función GetLetterCount para contar cuántas letras hay en una cadena de caracteres Unicode. Ha sido compilada para C# 8.0 bajo un contexto anulable, y el único parámetro de la función es una cadena para la cual el valor null es aceptable.

La entrada de hoy es continuación de la entrega anterior de la serie, y versa sobre los atributos de anulabilidad (nullability attributes), el conjunto de atributos que se añadieron a la librería de .NET como parte del soporte para las referencias anulables. Estos atributos pueden dividirse en dos categorías:

  • Si conoce usted los fundamentos de la arquitectura de .NET Framework y ha visto la especificación más reciente del Lenguaje Intermedio (Intermediate Language, IL) al que el código escrito en C# se transforma, sabrá que IL no ofrece soporte directo alguno para los tipos anulables. Por esta razón, cuando compilamos código que utiliza tipos anulables, el compilador de C# se apoya en dos atributos especialmente definidos en la librería base para anotar la anulabilidad de las entidades usadas en nuestro código (en particular, de los parámetros y valores de retorno de los métodos): NullableAttribute y NullableContextAttribute (espacio de nombres System.Runtime.CompilerServices). Por ejemplo, si se desensambla la librería anterior utilizando ILSpy, se podrá ver que la implementación de la función comienza con las siguientes líneas:
// Methods
.method public hidebysig static
int32 GetLetterCount (
    string source
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
    01 00 02 00 00
)
// ...

Observe que el tipo del parámetro en el encabezamiento a nivel de IL es simplemente string (no string?), y que la función comienza construyendo una instancia del atributo NullableContext, cuyo argumento codifica la posible anulabilidad del parámetro y el tipo de retorno. En este enlace se puede encontrar información detallada sobre qué significa esa codificación.

Estos atributos predefinidos juegan obviamente un rol especial durante la compilación, lo que los convierte de facto en una extensión del sistema de tipos de C#, y no pueden ser utilizados directamente por el programador; por esta razón, que no hablaremos mucho más sobre ellos aquí.

  • Ya hemos hablado en ocasiones anteriores acerca de que el compilador no es omnisapiente, y que frecuentemente (y muy especialmente a través de las fronteras de diferentes ensamblados) es necesario y conveniente suplir la información que el compilador puede detectar (a través de los caracteres ? insertados detrás de los nombres de tipos por referencia) con conocimiento adicional relativo a la anulabilidad o no anulabilidad de las entidades que forman parte de nuestro código. Para ello, la librería base ha incorporado, como parte del espacio de nombres System.Diagnostics.CodeAnalysis, otro conjunto de atributos que permiten suministrar al compilador los hints adicionales necesarios para realizar un mejor análisis de flujo asociado a la anulabilidad. En esta línea, el equipo de desarrollo de .NET viene desde hace algún tiempo anotando detalladamente las librerías con tales atributos.

Supongamos, por ejemplo, que el código original no definiera el parámetro source como string?, sino simplemente como string. Una posible causa podría ser que existe código externo (potencialmente, de terceros) que utiliza actualmente la librería y pudiera estar pasando null como valor al método. En cualquier caso, nosotros, que lo hemos implementado, sabemos que el método incluye una llamada a IsNullOrWhitespace que nos cubre en caso de que el valor null fuera pasado como argumento. ¿Cómo indicarle al compilador que se puede ahorrar las advertencias de anulabilidad asociadas al código cliente, porque nada malo debe pasar? Precisamente para situaciones como ésta es que se ha creado el atributo AllowNull:

namespace CS90Lib
{
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    
    public static class StringFunctions
    {
        public static int GetLetterCount([AllowNullstring source)
        {
            if (string.IsNullOrWhiteSpace(source))
            {
                return 0;
            }
            Debug.Assert(source != null && source.Length > 0);
            return source.Count(ch => char.IsLetter(ch));
        }
    }
}

Así lucirá en este caso el correspondiente código IL:

// Methods
.method public hidebysig static
int32 GetLetterCount (
    string source
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
    01 00 01 00 00
)
.param [1]
    .custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.AllowNullAttribute::.ctor() = (
        01 00 00 00 
    )
// ...

Sin el atributo AllowNull, el código que haga llamadas «sospechosas» a la función GetLetterCount recibirá mensajes de advertencia como los siguientes:

CS8604: Possible null reference argument for parameter ‘source’ in StringFunctions.GetLetterCount(string source).

CS8625: Cannot convert null literal to non-nullable reference type.

Con el atributo AllowNull presente, tales advertencias no se generarán.

Al estilo de AllowNull, existe otra decena de atributos que permiten codificar información avanzada de anulabilidad en los metadatos de nuestras entidades. Estos atributos representan, en manos del programador, potentes herramientas con las que expresar contratos a cumplir entre nuestro código y el código que nos llama. Puede encontrar una referencia completa de estos atributos aquí.

Con esta entrada, damos por concluida la serie dedicada a las referencias anulables; habrá que buscar un tema interesante (que siempre los hay) para la próxima. ¡Hasta entonces!

奥克塔维奥


Referencia musical: No me extenderé mucho aquí sobre Kansas, mi banda favorita. «Paradox» es el segundo tema de ese excelente disco que fue el «Point of Know Return» (1977), que llegó hasta el número 4 de la lista Billboard aupado por el éxito del tema «Dust in the Wind«. Si le gusta el grupo y el rock de esa época, le recomiendo que no se pierda el «Point of Know Return: Live and Beyond» (2021), una grabación en directo en la que la formación actual de la banda interpreta el disco original en su totalidad, en conmemoración de su 40-mo aniversario.

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 *