Exercício – aplicar estratégias de segurança nula
Na unidade anterior, você aprendeu a expressar a intenção de nulidade no código. Nesta unidade, você aplicará o que aprendeu a um projeto C# existente.
Observação
Este módulo usa a CLI (interface de linha de comando) do .NET e o Visual Studio Code para desenvolvimento local. Depois de concluir este módulo, você pode aplicar os conceitos usando o Visual Studio (Windows), o Visual Studio para Mac (macOS) ou o desenvolvimento contínuo usando o Visual Studio Code (Windows, Linux, & macOS).
Este módulo usa o SDK do .NET 6.0. Verifique se tem o .NET 6.0 instalado, executando o seguinte comando em seu terminal preferido:
dotnet --list-sdks
Saída semelhante à seguinte exibida:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
Verifique se uma versão que começa com 6
está listada. Se nenhum estiver listado ou o comando não for encontrado, instale o SDK do .NET 6.0 mais recente.
Recuperar e examinar o código de exemplo
Em um terminal de comando, clone o exemplo de repositório do GitHub e alterne para o diretório clonado.
git clone https://github.com/microsoftdocs/mslearn-csharp-null-safety cd mslearn-csharp-null-safety
Abra o diretório do projeto no Visual Studio Code.
code .
Execute o projeto de exemplo usando o comando
dotnet run
.dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Isso resultará na geração de 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
O rastreamento de pilha indica que a exceção ocorreu na linha 13 em .\src\ContosoPizza.Service\Program.cs. Na linha 13, o método
Add
é chamado na propriedadepizza.Cheeses
. Comopizza.Cheeses
énull
, é gerado 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! */
Habilitar contexto anulável
Agora você habilitará um contexto anulável e examinará seu efeito na compilação.
Em src/ContosoPizza.Service/ContosoPizza.Service.csproj, adicione a linha realçada e salve as alterações:
<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>
A alteração anterior habilita o contexto anulável para todo o projeto do
ContosoPizza.Service
.Em src/ContosoPizza.Models/ContosoPizza.Models.csproj, adicione a linha realçada e salve as alterações:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
A alteração anterior habilita o contexto anulável para todo o projeto do
ContosoPizza.Models
.Crie a solução de exemplo usando o comando
dotnet build
.dotnet build
A compilação tem êxito com dois avisos.
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
Crie a solução de exemplo novamente usando o comando
dotnet build
.dotnet build
Desta vez, a compilação tem êxito sem erros ou avisos. A compilação anterior foi concluída com êxito, com avisos. Como a origem não foi alterada, o processo de compilação não executa o compilador novamente. Como a compilação não executa o compilador, não há nenhum aviso.
Dica
Você pode forçar uma recompilação de todos os assemblies em um projeto usando o comando
dotnet clean
anterior aodotnet build
.Nos arquivos .csproj, adicione as linhas realçadas e salve as alterações.
<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>
As alterações anteriores instruem o compilador a falhar na compilação sempre que um aviso é encontrado.
Dica
O uso de
<TreatWarningsAsErrors>
é opcional. No entanto, recomendamos isso, pois garante que você não ignore nenhum aviso.Crie a solução de exemplo usando o comando
dotnet build
.dotnet build
A compilação falha com 2 erros.
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
Ao tratar avisos como erros, o aplicativo não é mais compilado. Isso, na verdade, é desejado nessa situação, pois o número de erros é pequeno e nós os resolveremos rapidamente. Os dois erros (CS8618) permitem que você saiba que há propriedades declaradas como não anuláveis que ainda não foram inicializadas.
Corrigir os erros
Há várias táticas para resolver os avisos/os erros relacionados à nulidade. Alguns exemplos incluem:
- Exigir uma coleção não anulável de queijos e coberturas como parâmetros do construtor
- Interceptar a propriedade
get
/set
e adicionar uma verificação denull
- Expressar a intenção de que as propriedades sejam anuláveis
- Inicializar a coleção com um valor padrão (vazio) embutido usando inicializadores de propriedade
- Atribuir um valor padrão (vazio) à propriedade no construtor
Para corrigir o erro na propriedade
Pizza.Cheeses
, modifique a definição de propriedade em Pizza.cs para adicionar uma verificação denull
. Uma pizza não é realmente uma pizza sem queijo, não é?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(); }
No código anterior:
- Um novo campo de apoio é adicionado para ajudar a interceptar os acessadores de propriedade
get
eset
chamados de_cheeses
. Ele é declarado como anulável (?
) e deixado sem inicializar. - O acessador
get
é mapeado para uma expressão que usa o operador de avaliação de nulo (??
). Essa expressão retorna o campo_cheeses
, supondo que não sejanull
. Se fornull
, ele atribuirá_cheeses
comonew List<PizzaCheese>()
antes de retornar_cheeses
. - O acessador
set
também é mapeado para uma expressão e faz uso do operador de avaliação de nulo. Quando um consumidor atribui um valornull
, o ArgumentNullException é gerado.
- Um novo campo de apoio é adicionado para ajudar a interceptar os acessadores de propriedade
Como nem todas as pizzas têm cobertura,
null
pode ser um valor válido para a propriedadePizza.Toppings
. Nesse caso, faz sentido expressá-la como anulável.Modifique a definição de propriedade em Pizza.cs para permitir que
Toppings
seja anulável.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(); }
A propriedade
Toppings
agora é expressa como anulável.Adicione a linha realçada 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! */
No código anterior, o operador de avaliação de nulo é usado para atribuir
Toppings
anew List<PizzaTopping>();
se fornull
.
Executar a solução concluída
Salve todas as suas alterações e, em seguida, compile a solução.
dotnet build
A compilação é concluída sem erros nem avisos.
Execute o aplicativo.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
O aplicativo é executado até a conclusão (sem erro) e exibe a seguinte saída:
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!
Resumo
Nesta unidade, você usou um contexto anulável para identificar e evitar possíveis ocorrências de NullReferenceException
no código.