Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 20 de junio de 2023
C#

Desde la aparición de los nullable reference types, o tipos referencia anulables, en C# 8, nos encontramos frecuentemente con el warning de compilación CS8618, que nos recuerda que las propiedades de tipo referencia definidas como no anulables (es decir, que no pueden contener nulos) deben ser inicializadas obligatoriamente porque de lo contrario contendrán el valor null, algo que iría en contra de su propia definición.

Para verlo con un ejemplo, consideremos una aplicación de consola con el siguiente código:

var friend = new Friend() {Name = "John", Age = 32};
Console.WriteLine($"Hello, {friend.Name}");

public class Friend
{
    public string Name { get; set; }
    public int Age { get; set; }
}

La aplicación se ejecutará sin problema, aunque al compilarla obtendremos el warning CS8618:

D:\Projects\ConsoleApp78>dotnet build
MSBuild version 17.4.1+9a89d02ff for .NET
  Determining projects to restore...
  Restored D:\Projects\ConsoleApp78\ConsoleApp78.csproj (in 80 ms).
D:\Projects\ConsoleApp78\Program.cs(8,19): warning CS8618: Non-nullable property 'Name' 
 must contain a non-null value when exiting constructor. Consider declaring the 
 property as nullable.
[...]

[D:\Projects\ConsoleApp78\ConsoleApp78.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.63

D:\Projects\ConsoleApp78\ConsoleApp78>_

También en Visual Studio podremos ver el subrayado marcando la propiedad como incorrecta:

Visual Studio mostrando el warning CS8618 sobre la propiedad

Aunque muchas veces este warning viene bien porque nos ayudará a evitar errores, hay otras ocasiones en las que puede llegar a ser molesta tanta insistencia. Y en estos casos, ¿cómo podemos librarnos de este aviso?

Disclaimer: algunas de las soluciones mostradas no son especialmente recomendables, o incluso no tienen sentido en la práctica, pero seguro que son útiles para ver características de uso poco habitual en C#.

1. Deshabilitar el aviso CS8618

Oye, ¿que nos molestan estos warnings? Pues sin problema, le decimos al compilador que ni nos informe cuando se produzcan, y hemos acabado con el problema de un plumazo 😜

Esto lo conseguimos de forma muy sencilla añadiendo el siguiente grupo de propiedades al archivo .csproj:

<PropertyGroup>
  <NoWarn>8618</NoWarn>
</PropertyGroup>

Esto podemos aplicarlo a varios avisos separando sus identificadores por comas. Podríamos hacerlo, por ejemplo, con otros warnings relacionados como los accesos a propiedades que podrían ser nulas (CS8602 o CS8625).

Por supuesto, no es una solución en absoluto recomendable porque los avisos se muestran por algo. Si en nuestro proyecto estamos usando tipos referencia anulables, este warning es útil para saber dónde podemos estar cometiendo errores, y ocultar el aviso no nos va a ayudar a solucionarlos.

2. Deshabilitar el soporte para tipos referencia anulables a nivel de proyecto

Vale, sigue siendo matar moscas a cañonazos pero, indudablemente, si deshabilitamos los tipos referencia anulables, el problema habrá desaparecido para siempre en todas las clases de la aplicación.

Esto podemos conseguirlo simplemente estableciendo la propiedad <Nullable> del archivo .csproj a disable, que viene habilitado por defecto en todos los nuevos proyectos .NET:

<PropertyGroup>
  ...
  <Nullable>disable</Nullable>
  ...
</PropertyGroup>

Tras realizar este cambio, el aviso desaparecerá de los resultados de la compilación, aunque obviamente estaremos renunciando a los beneficios del control de nulos, que siempre aportan seguridad en los desarrollos.

Esta solución puede ser razonable para código legacy, que ya se iniciaron sin soporte para tipos referencia anulables y que no estamos en disposición de migrar. Pero en un proyecto nuevo, donde se está empezando desde cero, no tiene sentido deshabilitar esta característica porque está pensada para ayudarnos a lidiar con los valores nulos de forma más segura.

3. Deshabilitar el soporte para tipos referencia anulables a nivel de bloque de código

Si queremos hilar algo más fino, también podríamos deshabilitar los tipos referencia anulables a nivel de archivo, o incluso de bloque de código, usando la directiva #nullable para deshabilitar y volver a habilitar la comprobación de nulos:

#nullable disable
public class Friend
{
    public string Name { get; set; }

    public int Age { get; set; }
}
#nullable enable

Esta fórmula es mejor que la anterior porque al menos estamos desactivando esta característica sólo en el archivo o porción de código que nos interese, aunque sigue siendo una solución bastante tosca y poco razonable en la mayoría de las ocasiones.

4. Inicializar el miembro a nulo, pero hacer parecer que no lo es ;)

Aunque parezca extraño, el siguiente código también eliminaría el aviso del compilador:

public class Friend
{
    public string Name { get; set; } = default!;
    public int Age { get; set; }
}

O, lo que es lo mismo,

public class Friend
{
    public string Name { get; set; } = null!;
    public int Age { get; set; }
}

Esta solución es un poco surrealista, pues de alguna forma estamos incurriendo en una contradicción a nivel de código: por un lado, si estamos usando tipos referencia anulables e indicamos que Name es string y no string? es porque de alguna forma queremos que no contenga valores nulos... pero luego le asignamos un nulo, usando el null forgiving operator (!) para indicar al compilador que ignore el asunto porque le aseguramos que es un valor no nulo.

Por tanto, utilidad práctica de esto: probablemente cero. Sin embargo, sí me parece interesante que exista la posibilidad sintáctica de hacerlo 😜

5. Usar miembros requeridos

Esta solución hace uso de algo aparentemente no relacionado, como los miembros de inicialización obligatoria o required members de C# 11.

La idea consiste en aplicar el modificador required a la definición de la propiedad Name, indicando así que ésta debe ser inicializada obligatoriamente al instanciar el objeto.

public class Friend
{
    public required string Name { get; set; }
    public int Age { get; set; }
}

De esta forma, el compilador ya no protestará en la declaración de la clase, puesto que se delega al código que la instancie la responsabilidad de especificar un valor no nulo para la propiedad Name. Es decir, si declaramos la clase Friend como en el bloque de arriba, el compilador obligará a que la propiedad Name sea inicializada en el momento de crear el objeto, y ofrecerá el aviso CS8618 si el valor es nulo o podría serlo:

// Error CS9035: Required member 'Name' must be set:
var friend = new Friend(); 

// Warning CS8625: Cannot convert null literal to non-nullable reference type:
var friend = new Friend { Name = null }; 

// Warning CS8601: Possible null reference assignment:
var friend = new Friend() { Name = a==b? null: "John" }; 

// Ok!
var friend = new Friend() { Name = "John" };

6. Declarar la propiedad como anulable

Releamos el texto del aviso CS8618:

Non-nullable property 'Name' must contain a non-null value when exiting constructor. 
Consider declaring the property as nullable.

Como podemos ver, aquí mismo se nos está dando pistas sobre una de las formas más sencillas de solucionar el problema: declarar la propiedad como anulable. En la práctica, esto solo consiste en añadir el sufijo ? (carácter interrogación) al tipo de la propiedad, como podemos ver a continuación:

public class Friend
{
    public string? Name { get; set; }

    public int Age { get; set; }
}

Con esto estamos indicando al compilador que sabemos que la propiedad Name puede contener un nulo, por lo que éste dejará de mostrar el warning.

Eso sí, a partir de este momento tendremos que responsabilizarnos de gestionar este posible valor nulo, o propagar la nulabilidad a otras partes del código. De lo contrario seguiremos recibiendo avisos en tiempo de compilación, como en el siguiente ejemplo, donde vemos que el intento de acceder al método ToUpper() del nombre del amigo generará un nuevo warning:

var friend = new Friend();
var upperName = friend.Name.ToUpper(); // Warning CS8602: Dereference of 
                                       // a possibly null reference.

Console.WriteLine($"Hello, {upperName}");

public class Friend
{
    public string? Name { get; set; }
    public int Age { get; set; }
}

7. Inicializar las propiedades en el constructor o al declararlas

También del texto del propio warning CS8618 podemos ver claramente otra solución al problema, la que sin duda es la mejor de todas: inicializar la propiedad a un valor no nulo en el constructor de la clase. Aunque sea más trabajosa, realmente es la única forma en la que estamos manteniendo la intención original del tipo de la propiedad, que es no permitir nulos, y al mismo tiempo ofreciéndole un valor que entendemos razonable para la misma en función del dominio de la aplicación.

En la práctica, esto quiere decir que un código como el siguiente eliminaría el aviso de la propiedad Name:

public class Friend
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Friend()
    {
        Name = "Anonymous";
    }
}

O también inicializando la propiedad justo en el momento de su declaración:

public class Friend
{
    public string Name { get; set; } = "Anonymous";
    public int Age { get; set; }
}

Ambas fórmulas, totalmente equivalentes, harán que el aviso del compilador desaparezca, pues el miembro Name contendrá un valor no nulo en el momento de la construcción del objeto. Y sin duda, son las opciones más recomendables porque estaremos especificando un valor inicial no nulo para una propiedad que ha sido modelada para que deba ser así.

¡Espero que os haya resultado interesante!

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!