Ejercicio: Aplicación de estrategias de seguridad de valores NULL
En la unidad anterior, ha aprendido a expresar la intención de nulabilidad en el código. En esta unidad, aplicará lo que ha aprendido a un proyecto de C# existente.
Nota:
En este módulo se usan la CLI (interfaz de la línea de comandos) de .NET y Visual Studio Code para el desarrollo local. Cuando complete este módulo, podrá aplicar los conceptos mediante Visual Studio (Windows), Visual Studio para Mac (macOS), o seguir con el desarrollo mediante Visual Studio Code (Windows, Linux y macOS).
En este módulo se usa el SDK de .NET 6.0. Asegúrese de que tiene instalado .NET 6.0 mediante la ejecución del siguiente comando en la terminal que prefiera:
dotnet --list-sdks
Verá un resultado similar al siguiente:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
Asegúrese de que aparezca una versión que comience en 6
. Si no aparece ninguna o no se encuentra el comando, instale el SDK más reciente de .NET 6.0.
Recuperación y examen del código de ejemplo
En un terminal de comandos, clone el repositorio de GitHub ejemplo y cambie al directorio clonado.
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safety
Abra el directorio del proyecto en Visual Studio Code.
code .
Ejecute el proyecto de ejemplo mediante el comando
dotnet run
.dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Como resultado, se produce NullReferenceException.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in .\src\ContosoPizza.Service\Program.cs:line 13
El seguimiento de la pila indica que la excepción se produjo en la línea 13 de .\src\ContosoPizza.Service\Program.cs. En la línea 13, se llama al método
Add
en la propiedadpizza.Cheeses
. Puesto quepizza.Cheeses
esnull
, se produce NullReferenceException.using ContosoPizza.Models; // Create a pizza Pizza pizza = new("Meat Lover's Special") { Size = PizzaSize.Medium, Crust = PizzaCrust.DeepDish, Sauce = PizzaSauce.Marinara, Price = 17.99m, }; // Add cheeses pizza.Cheeses.Add(PizzaCheese.Mozzarella); pizza.Cheeses.Add(PizzaCheese.Parmesan); // Add toppings pizza.Toppings.Add(PizzaTopping.Sausage); pizza.Toppings.Add(PizzaTopping.Pepperoni); pizza.Toppings.Add(PizzaTopping.Bacon); pizza.Toppings.Add(PizzaTopping.Ham); pizza.Toppings.Add(PizzaTopping.Meatballs); Console.WriteLine(pizza); /* Expected output: The "Meat Lover's Special" is a deep dish pizza with marinara sauce. It's covered with a blend of mozzarella and parmesan cheese. It's layered with sausage, pepperoni, bacon, ham and meatballs. This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49! */
Habilitación de un contexto que acepta valores NULL
Ahora habilitará un contexto que acepta valores NULL y examinará su efecto en la compilación.
En src/ContosoPizza.Service/ContosoPizza.Service.csproj, agregue la línea resaltada y guarde los cambios:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" /> </ItemGroup> </Project>
El cambio anterior habilita el contexto que acepta valores NULL para todo el proyecto
ContosoPizza.Service
.En src/ContosoPizza.Models/ContosoPizza.Models.csproj, agregue la línea resaltada y guarde los cambios:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
El cambio anterior habilita el contexto que acepta valores NULL para todo el proyecto
ContosoPizza.Models
.Compile la solución de ejemplo mediante el comando
dotnet build
.dotnet build
La compilación se realiza correctamente con dos advertencias.
dotnet build Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... Restored .\src\ContosoPizza.Service\ContosoPizza.Service.csproj (in 477 ms). Restored .\src\ContosoPizza.Models\ContosoPizza.Models.csproj (in 475 ms). .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] ContosoPizza.Models -> .\src\ContosoPizza.Models\bin\Debug\net6.0\ContosoPizza.Models.dll ContosoPizza.Service -> .\src\ContosoPizza.Service\bin\Debug\net6.0\ContosoPizza.Service.dll Build succeeded. .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] .\src\ContosoPizza.Models\Pizza.cs(3,28): warning CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] 2 Warning(s) 0 Error(s) Time Elapsed 00:00:07.48
Compile la solución de ejemplo usando de nuevo el comando
dotnet build
.dotnet build
Esta vez, la compilación se realiza correctamente sin errores ni advertencias. La compilación anterior se completó correctamente, con advertencias. Puesto que el origen no cambió, el proceso de compilación no vuelve a ejecutar el compilador. Puesto que la compilación no ejecuta el compilador, no hay advertencias.
Sugerencia
Puede forzar una recompilación de todos los ensamblados de un proyecto mediante el comando
dotnet clean
anterior adotnet build
.En los archivos .csproj, agregue las líneas resaltadas y guarde los cambios.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\ContosoPizza.Models\ContosoPizza.Models.csproj" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> </Project>
Los cambios anteriores indicarán al compilador que genere un error en la compilación cada vez que se encuentre una advertencia.
Sugerencia
El uso de
<TreatWarningsAsErrors>
es opcional. Sin embargo, es recomendable, ya que garantiza que no pase por alto las advertencias.Compile la solución de ejemplo mediante el comando
dotnet build
.dotnet build
Se produce un error en la compilación con dos errores.
dotnet build Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... All projects are up-to-date for restore. .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] Build FAILED. .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Cheeses' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] .\src\ContosoPizza.Models\Pizza.cs(3,28): error CS8618: Non-nullable property 'Toppings' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. [.\src\ContosoPizza.Models\ContosoPizza.Models.csproj] 0 Warning(s) 2 Error(s) Time Elapsed 00:00:02.95
Al tratar las advertencias como errores, la aplicación ya no se compila. De hecho, esto es lo que se desea en esta situación, ya que el número de errores es pequeño y los abordaremos rápidamente. Los dos errores (CS8618) le permiten saber que hay propiedades declaradas como que no aceptan valores NULL que aún no se han inicializado.
Corrección de errores
Hay una gran cantidad de tácticas para resolver las advertencias o los errores relacionados con la nulabilidad. Estos son algunos ejemplos:
- Requerir una colección que no acepta valores NULL de queso e ingredientes como parámetros de constructor
- Interceptar la propiedad
get
/set
y agregue una comprobación denull
- Expresar la intención de que las propiedades acepten valores NULL
- Inicializar la colección con un valor predeterminado (vacío) insertado mediante inicializadores de propiedad
- Asignar a la propiedad un valor predeterminado (vacío) en el constructor
Para corregir el error en la propiedad
Pizza.Cheeses
, modifique la definición de propiedad en Pizza.cs para agregar una comprobación denull
. No es realmente una pizza sin queso, ¿verdad?namespace ContosoPizza.Models; public sealed record class Pizza([Required] string Name) { private ICollection<PizzaCheese>? _cheeses; public int Id { get; set; } [Range(0, 9999.99)] public decimal Price { get; set; } public PizzaSize Size { get; set; } public PizzaCrust Crust { get; set; } public PizzaSauce Sauce { get; set; } public ICollection<PizzaCheese> Cheeses { get => (_cheeses ??= new List<PizzaCheese>()); set => _cheeses = value ?? throw new ArgumentNullException(nameof(value)); } public ICollection<PizzaTopping>? Toppings { get; set; } public override string ToString() => this.ToDescriptiveString(); }
En el código anterior:
- Se agrega un nuevo campo de respaldo para ayudar a interceptar los descriptores de acceso de propiedad
get
yset
denominados_cheeses
. Se declara como que acepta valores NULL (?
) y se deja sin inicializar. - El descriptor de acceso
get
se asigna a una expresión que usa el operador de fusión de NULL (??
). Esta expresión devuelve el campo_cheeses
, suponiendo que no seanull
. Si esnull
, se asigna_cheeses
anew List<PizzaCheese>()
antes de devolver_cheeses
. - El descriptor de acceso
set
también se asigna a una expresión y usa el operador de fusión de NULL. Cuando un consumidor asigna un valornull
, se produce ArgumentNullException.
- Se agrega un nuevo campo de respaldo para ayudar a interceptar los descriptores de acceso de propiedad
Puesto que no todas las pizzas tienen ingredientes,
null
puede ser un valor válido para la propiedadPizza.Toppings
. En este caso, tiene sentido expresarlo como que acepta valores NULL.Modifique la definición de propiedad en Pizza.cs para permitir que
Toppings
acepte valores NULL.namespace ContosoPizza.Models; public sealed record class Pizza([Required] string Name) { private ICollection<PizzaCheese>? _cheeses; public int Id { get; set; } [Range(0, 9999.99)] public decimal Price { get; set; } public PizzaSize Size { get; set; } public PizzaCrust Crust { get; set; } public PizzaSauce Sauce { get; set; } public ICollection<PizzaCheese> Cheeses { get => (_cheeses ??= new List<PizzaCheese>()); set => _cheeses = value ?? throw new ArgumentNullException(nameof(value)); } public ICollection<PizzaTopping>? Toppings { get; set; } public override string ToString() => this.ToDescriptiveString(); }
La propiedad
Toppings
ahora se expresa como que acepta valores NULL.Agregue la línea resaltada a ContosoPizza.Service\Program.cs:
using ContosoPizza.Models; // Create a pizza Pizza pizza = new("Meat Lover's Special") { Size = PizzaSize.Medium, Crust = PizzaCrust.DeepDish, Sauce = PizzaSauce.Marinara, Price = 17.99m, }; // Add cheeses pizza.Cheeses.Add(PizzaCheese.Mozzarella); pizza.Cheeses.Add(PizzaCheese.Parmesan); // Add toppings pizza.Toppings ??= new List<PizzaTopping>(); pizza.Toppings.Add(PizzaTopping.Sausage); pizza.Toppings.Add(PizzaTopping.Pepperoni); pizza.Toppings.Add(PizzaTopping.Bacon); pizza.Toppings.Add(PizzaTopping.Ham); pizza.Toppings.Add(PizzaTopping.Meatballs); Console.WriteLine(pizza); /* Expected output: The "Meat Lover's Special" is a deep dish pizza with marinara sauce. It's covered with a blend of mozzarella and parmesan cheese. It's layered with sausage, pepperoni, bacon, ham and meatballs. This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49! */
En el código anterior, el operador de fusión de NULL se usa para asignar
Toppings
anew List<PizzaTopping>();
si esnull
.
Ejecución de la solución completada
Guarde todos los cambios y, a continuación, compile la solución.
dotnet build
La compilación se completa sin errores ni advertencias.
Ejecutar la aplicación.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
La aplicación se ejecuta hasta su finalización (sin errores) y muestra la siguiente salida:
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj The "Meat Lover's Special" is a deep dish pizza with marinara sauce. It's covered with a blend of mozzarella and parmesan cheese. It's layered with sausage, pepperoni, bacon, ham and meatballs. This medium size is $17.99. Delivery is $2.50 more, bringing your total $20.49!
Resumen
En esta unidad, ha usado un contexto que acepta valores NULL para identificar y evitar posibles ocurrencias de NullReferenceException
en el código.