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-safetyAbra o diretório do projeto no Visual Studio Code.
code .Execute o projeto de exemplo usando o
dotnet runcomando.dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csprojIsso 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 13O rastreamento de pilha indica que a exceção ocorreu na linha 13 em .\src\ContosoPizza.Service\Program.cs. Na linha 13, o
Addmétodo é chamado napizza.Cheesespropriedade. 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.Serviceo 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.Modelso projeto.Crie a solução de exemplo usando o
dotnet buildcomando.dotnet buildA 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.48Crie a solução de exemplo novamente usando o
dotnet buildcomando.dotnet buildDesta 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 cleancomando 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 buildcomando.dotnet buildA 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.95Ao 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/sete 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.Cheeseserro na propriedade, modifique a definição da propriedade em Pizza.cs para adicionar umanullverificaçã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
getacessadores de propriedade esetnomeados_cheeses. É declarado como anulável (?) e deixado sem inicialização. - O
getacessador é mapeado para uma expressão que usa o operador de coalescência nula (??). Esta expressão retorna o_cheesescampo, supondo que nãonullseja . Se for, ele atribuinulla_cheesesantes denew List<PizzaCheese>()voltar_cheeses. - O
setacessador também é mapeado para uma expressão e faz uso do operador null-coalescing. Quando um consumidor atribui umnullvalor, o ArgumentNullException é lançado.
- Um novo campo de suporte é adicionado para ajudar a intercetar os
Como nem todas as pizzas têm coberturas,
nullpode ser um valor válido para oPizza.Toppingsimóvel. Neste caso, faz sentido expressá-lo como sendo anulável.Modifique a definição de propriedade em Pizza.cs para permitir que
Toppingsseja 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
Toppingspropriedade é 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
Toppingsanew List<PizzaTopping>();se énull.
Execute a solução concluída
Salve todas as suas alterações e, em seguida, crie a solução.
dotnet buildA compilação é concluída sem avisos ou erros.
Executar a aplicação.
dotnet run --project src/ContosoPizza.Service/ContosoPizza.Service.csprojO 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.