Noções básicas sobre nulidade

Concluído

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

No exemplo a seguir, a variável FooBar é atribuída a null e imediatamente desreferenciada, exibindo 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 identificar como desenvolvedor quando seus aplicativos crescem em tamanho e complexidade. Identificar possíveis erros como esse é um trabalho para ferramentas e o compilador do C# está aqui para ajudar.

Definindo a segurança nula

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

Considerando o exemplo FooBar anterior, você pode evitar o NullReferenceException verificando se a variável fooBar era null antes de desreferenciá-la:

// 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 a identificar cenários como esse, o compilador pode inferir a intenção do código e impor o comportamento desejado. No entanto, isso ocorre somente quando um contexto que permite valor nulo é habilitado. Antes de discutir o contexto que permite valor nulo, vamos descrever os possíveis tipos que permitem valor nulo.

Tipos anuláveis

Antes do C# 2.0, somente os tipos de referência eram anuláveis. Os tipos de valor como int ou DateTime não podem ser null. Se esses tipos são inicializados sem um valor, eles fazem fallback para seu valor default. No caso de um int, é 0. Para um DateTime, é DateTime.MinValue.

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

Considere o trecho de C# a seguir:

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 de referência string foi declarado, mas nenhuma atribuição foi feita.
  • second é atribuído string.Empty quando é declarado. O objeto nunca teve uma atribuição null.
  • third é 0, apesar de não ter sido atribuído. É um (tipo-valor) struct e tem um valor default de 0.
  • date tem a inicialização cancelada, mas o valor default dele é System.DateTime.MinValue.

A partir do C# 2.0, você pode definir tipos que permitem valor nulo usando Nullable<T> (ou T? para resumir). Isso permite que os tipos de valor sejam anuláveis. Considere o trecho de C# a seguir:

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 que permite valor nulo é não reinicializado.
  • second é atribuído null quando é declarado.
  • third é null assim como o valor default para Nullable<int> é null.
  • fourth é 0 assim como a expressão new() chama o construtor Nullable<int> e int é 0 por padrão.

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

Considere o trecho de C# a seguir:

#nullable enable

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

Considerando o exemplo anterior, o compilador infere sua intenção da seguinte forma:

  • first nunca é null como ele é definitivamente atribuído.
  • second nunca deve ser null, mesmo que seja inicialmente null. Avaliando second antes de atribuir um valor resulta em um aviso do compilador, pois ele é não inicializado.
  • third pode ser null. Por exemplo, ele pode apontar para um System.String, mas pode apontar para null. Qualquer uma dessas variações é aceitável. O compilador ajuda você com um aviso se você desreferenciar third sem primeiro verificar se ele não é nulo.

Importante

Para usar o recurso de tipos de referência que permitem valor nulo, conforme mostrado acima, ele deve estar dentro de um contexto que permite valor nulo. Isso é detalhado na próxima seção.

Contexto que permite valor nulo

Contextos que permitem valor nulo habilitam o controle refinado para a maneira como o compilador interpreta variáveis de tipo de referência. Há quatro contextos anuláveis possíveis:

  • disable: o compilador se comporta da mesma forma que o C# 7.3 e anterior.
  • enable: o compilador habilita toda a análise de referência nula e todos os recursos de linguagem.
  • warnings: o compilador executa todas as análises nulas e emite avisos quando o código pode ser desreferenciado null.
  • annotations: o compilador não executa análise nula ou emite avisos quando o código pode ser desreferenciadonull, mas você ainda pode anotar seu código usando tipos de referência anuláveis ? e operadores tolerantes a nulos (!).

Este módulo tem como escopo os contextos anuláveis disable ou enable. Para obter mais informações, veja Tipos de referência anuláveis: Contextos anuláveis.

Habilitar tipos de referência anuláveis

No arquivo de projeto C# (.csproj), adicione um nó filho <Nullable> ao elemento <Project> (ou acrescente a um <PropertyGroup> existente). Isso aplicará o contexto anulável enable 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 fazer o escopo de contexto anulável 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 ele reside. Para obter mais informações, consulte Tipos de referência anuláveis: contextos anuláveis (documentação)

Importante

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

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

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

    Captura de tela do aviso CS8600 do C#: Conversão de literal nula ou possível valor nulo em tipo não anulável.

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

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

Importante

Não há segurança nula garantida, mesmo se você reagir e eliminar todos os avisos. Há alguns cenários limitados que passarão na análise do compilador, ainda que resultem em uma NullReferenceException de runtime.

Resumo

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