Entendendo a anulabilidade

Concluído

Se você é um desenvolvedor .NET, é provável que tenha encontrado o System.NullReferenceException. Isso ocorre em tempo de execução quando a null é desreferenciada, ou seja, quando uma variável é avaliada em tempo de execução, mas a variável se refere a null. Essa exceção é, de longe, a exceção mais comum dentro do ecossistema .NET. O criador de , Sir Tony Hoare, refere-se como null o "erro de bilhões de nulldólares".

No exemplo a seguir, a FooBar variável é atribuída e null imediatamente desreferenciada, exibindo assim o problema:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

O problema se torna muito mais difícil de detetar como desenvolvedor quando seus aplicativos crescem em tamanho e complexidade. Detetar possíveis erros como este é um trabalho para ferramentas, e o compilador C# está aqui para ajudar.

Definição de segurança nula

O termo segurança nula define um conjunto de recursos específicos para tipos anuláveis que ajudam a reduzir o número de ocorrências possíveis NullReferenceException .

Considerando o exemplo anterior FooBar , você poderia evitar verificar NullReferenceException se a fooBar variável estava null antes de desreferenciar:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

Para ajudar na identificação de cenários como este, o compilador pode inferir a intenção do seu código e impor o comportamento desejado. No entanto, isso ocorre apenas quando um contexto anulável está habilitado. Antes de discutir o contexto anulável, vamos descrever os possíveis tipos anuláveis.

Tipos anuláveis

Antes do C# 2.0, apenas os tipos de referência eram anuláveis. Tipos de valor como int ou DateTime não poderiam ser null. Se esses tipos forem inicializados sem um valor, eles retornarão ao seu default valor. No caso de um int, isto é 0. Para um DateTime, é DateTime.MinValue.

Os tipos de referência instanciados sem valores iniciais funcionam de forma diferente. O default valor para todos os tipos de referência é null.

Considere o seguinte trecho em C#:

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

No exemplo anterior:

  • first é null porque o tipo string de referência foi declarado, mas nenhuma atribuição foi feita.
  • second é atribuído string.Empty quando é declarado. O objeto nunca teve uma null atribuição.
  • third é 0 apesar de não ter sido atribuído. É um struct (tipo de valor) e tem um default valor de 0.
  • date não foi inicializado, mas seu default valor é System.DateTime.MinValue.

A partir do C# 2.0, você pode definir tipos de valores anuláveis usando Nullable<T> (ou T? para abreviação). Isso permite que os tipos de valor sejam anuláveis. Considere o seguinte trecho em C#:

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

No exemplo anterior:

  • first é null porque o tipo de valor nulo não foi inicializado.
  • second é atribuído null quando é declarado.
  • third é null como o default valor para Nullable<int> é null.
  • fourth é 0 como a new() expressão chama o Nullable<int> construtor e int é 0 por padrão.

O C# 8.0 introduziu tipos de referência anuláveis, onde você pode expressar sua intenção de que um tipo de referência pode ser null ou é sempre não-null. Você pode estar pensando: "Eu pensei que todos os tipos de referência são anuláveis!" Você não está errado, e eles estão. Esse recurso permite que você expresse sua intenção, que o compilador tenta impor. A mesma T? sintaxe expressa que um tipo de referência deve ser anulável.

Considere o seguinte trecho em C#:

#nullable enable

string first = string.Empty;
string second;
string? third;

Dado o exemplo anterior, o compilador infere sua intenção da seguinte maneira:

  • firstnunca null é como é definitivamente atribuído.
  • secondnunca deve sernull, mesmo que seja inicialmentenull. Avaliar second antes de atribuir um valor resulta em um aviso do compilador, pois ele não é inicializado.
  • thirdpode ser null. Por exemplo, pode apontar para um System.String, mas pode apontar para null. Qualquer uma destas variações é aceitável. O compilador ajuda você avisando se você cancelar a referência third sem primeiro verificar se ele não é nulo.

Importante

Para usar o recurso de tipos de referência anuláveis como mostrado acima, ele deve estar dentro de um contexto anulável. Isso é detalhado na próxima seção.

Contexto anulável

Contextos anuláveis permitem um controle refinado de como o compilador interpreta variáveis de tipo de referência. Há quatro contextos anuláveis possíveis:

  • disable: O compilador se comporta de forma semelhante ao C# 7.3 e anteriores.
  • enable: O compilador permite todas as análises de referência nulas e todos os recursos de linguagem.
  • warnings: O compilador executa todas as análises nulas e emite avisos quando o código pode cancelar a referência null.
  • annotations: O compilador não executa análise nula ou emite avisos quando o código pode cancelar a referência null, mas você ainda pode anotar seu código usando tipos ? de referência anuláveis e operadores de tolerância nula (!).

Este módulo tem como escopo um ou disable enable outros contextos anuláveis. Para obter mais informações, consulte Nullable reference types: Nullable contexts.

Habilitar tipos de referência anuláveis

No arquivo de projeto C# (.csproj), adicione um nó filho <Nullable> ao <Project> elemento (ou acrescente a um existente <PropertyGroup>). Isso aplicará o enable contexto anulável a todo o projeto.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

Como alternativa, você pode definir o escopo de contexto nulo para um arquivo C# usando uma diretiva de compilador.

#nullable enable

A diretiva de compilador C# anterior é funcionalmente equivalente à configuração do projeto, mas tem como escopo o arquivo no qual reside. Para obter mais informações, consulte Tipos de referência anuláveis: contextos anuláveis (docs)

Importante

O contexto anulável é habilitado no arquivo .csproj por padrão em todos os modelos de projeto C# começando com .NET 6.0 e superior.

Quando o contexto anulável estiver habilitado, você receberá novos avisos. Considere o exemplo anterior FooBar , que tem dois avisos quando analisado em um contexto anulável:

  1. A FooBar fooBar = null; linha tem um aviso na null atribuição: C# Aviso CS8600: Convertendo literal nulo ou possível valor nulo para tipo não anulável.

    Screenshot do Aviso C# CS8600: Convertendo literal nulo ou possível valor nulo para tipo não anulável.

  2. A _ = fooBar.ToString(); linha também tem um aviso. Desta vez, o compilador está preocupado que fooBar pode ser nulo: C# Aviso CS8602: Desreferência de uma referência possivelmente nula.

    Captura de tela do Aviso C# CS8602: Desreferência de uma referência possivelmente nula.

Importante

Não há segurança nula garantida , mesmo que você reaja e elimine todos os avisos. Existem alguns cenários limitados que passarão na análise do compilador, mas resultarão em um tempo de execução NullReferenceException.

Resumo

Nesta unidade, você aprendeu a habilitar um contexto anulável em C# para ajudar a proteger contra NullReferenceExceptiono . Na próxima unidade, você aprenderá mais sobre como expressar explicitamente sua intenção em um contexto anulável.