Exercício - Aplicar estratégias de segurança nulas
Na unidade anterior, você aprendeu sobre como expressar sua intenção de anulação no código. Nesta unidade, você aplicará o que aprendeu a um projeto C# existente.
Nota
Este módulo usa a CLI do .NET (interface de linha de comando) e o Visual Studio Code para desenvolvimento local. Depois de concluir este módulo, você pode aplicar os conceitos usando Visual Studio (Windows), Visual Studio para Mac (macOS) ou desenvolvimento contínuo usando Visual Studio Code (Windows, Linux, & macOS).
Este módulo usa o SDK do .NET 6.0. Certifique-se de ter o .NET 6.0 instalado executando o seguinte comando no seu terminal preferido:
dotnet --list-sdks
Saída semelhante à seguinte aparece:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
Certifique-se de que 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 repositório GitHub de exemplo 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
dotnet run
comando.dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csproj
Isso resultará em um NullReferenceException arremesso.
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
Add
método é chamado napizza.Cheeses
propriedade. Desde quepizza.Cheeses
énull
, um NullReferenceException é jogado.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! */
Ativar 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
ContosoPizza.Service
o projeto.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
ContosoPizza.Models
o projeto.Crie a solução de exemplo usando o
dotnet build
comando.dotnet build
A compilação é bem-sucedida com 2 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
dotnet build
comando.dotnet build
Desta vez, a compilação é bem-sucedida sem erros ou avisos. A compilação anterior foi concluída com êxito, com avisos. Como o código-fonte não foi alterado, o processo de compilação não executa o compilador novamente. Como a compilação não executa o compilador, não há avisos.
Gorjeta
Você pode forçar uma reconstrução de todos os assemblies em um projeto usando o
dotnet clean
comando 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 a compilação sempre que um aviso for encontrado.
Gorjeta
O uso de
<TreatWarningsAsErrors>
é opcional. No entanto, recomendamos, pois garante que você não negligencie nenhum aviso.Crie a solução de exemplo usando o
dotnet build
comando.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 é de fato desejado nesta situação, pois o número de erros é pequeno e vamos resolvê-los rapidamente. Os dois erros (CS8618) informam que há propriedades declaradas como não anuláveis que ainda não foram inicializadas.
Corrigir os erros
Existem muitas táticas para resolver os avisos/erros relacionados à anulabilidade. Alguns exemplos incluem:
- Exigir uma coleção não anulável de queijos e coberturas como parâmetros do construtor
- Intercetar a propriedade
get
/set
e adicionar uma verificaçãonull
- Expresse 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
- Atribua à propriedade um valor padrão (vazio) no construtor
Para corrigir o
Pizza.Cheeses
erro na propriedade, modifique a definição da propriedade em Pizza.cs para adicionar umanull
verificação. 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 suporte é adicionado para ajudar a intercetar os
get
acessadores de propriedade eset
nomeados_cheeses
. É declarado como anulável (?
) e deixado sem inicialização. - O
get
acessador é mapeado para uma expressão que usa o operador de coalescência nula (??
). Esta expressão retorna o_cheeses
campo, supondo que nãonull
seja . Se for, ele atribui_cheeses
anew List<PizzaCheese>()
antes denull
voltar_cheeses
. - O
set
acessador também é mapeado para uma expressão e faz uso do operador null-coalescing. Quando um consumidor atribui umnull
valor, o ArgumentNullException é lançado.
- Um novo campo de suporte é adicionado para ajudar a intercetar os
Como nem todas as pizzas têm coberturas,
null
pode ser um valor válido para oPizza.Toppings
imóvel. Neste caso, faz sentido expressá-lo como sendo 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
Toppings
propriedade é agora expressa como sendo 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 coalescência nula é usado para atribuir
Toppings
anew List<PizzaTopping>();
se énull
.
Execute a solução concluída
Salve todas as suas alterações e, em seguida, crie a solução.
dotnet build
A compilação é concluída sem avisos ou erros.
Executar a aplicação.
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 prevenir possíveis NullReferenceException
ocorrências em seu código.