Exercício - Aplicar estratégias de segurança nulas

Concluído

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

  1. 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
    
  2. Abra o diretório do projeto no Visual Studio Code.

    code .
    
  3. 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 na pizza.Cheeses propriedade. Desde que pizza.Cheeses é , um NullReferenceException é nulljogado.

    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.

  1. 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.

  2. 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.

  3. 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
    
  4. 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 ao dotnet build.

  5. 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.

  6. 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ção null
  • 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
  1. Para corrigir o Pizza.Cheeses erro na propriedade, modifique a definição da propriedade em Pizza.cs para adicionar uma null 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 e set 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ão nullseja . Se for, ele atribui _cheeses a new List<PizzaCheese>() antes de nullvoltar_cheeses.
    • O set acessador também é mapeado para uma expressão e faz uso do operador null-coalescing. Quando um consumidor atribui um null valor, o ArgumentNullException é lançado.
  2. Como nem todas as pizzas têm coberturas, null pode ser um valor válido para o Pizza.Toppings imóvel. Neste caso, faz sentido expressá-lo como sendo anulável.

    1. 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.

    2. 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 a new List<PizzaTopping>(); se é null.

Execute a solução concluída

  1. Salve todas as suas alterações e, em seguida, crie a solução.

    dotnet build
    

    A compilação é concluída sem avisos ou erros.

  2. 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.