Tipos de referência anuláveis

Antes do C# 8.0, todos os tipos de referência eram anuláveis. Tipos de referência anuláveis referem-se a um grupo de recursos introduzidos no C# 8.0 que você pode usar para minimizar a probabilidade de que o código faça com que o runtime seja lançado System.NullReferenceException. Os tipos de referência anuláveis incluem três recursos que ajudam você a evitar essas exceções, incluindo a capacidade de marcar explicitamente um tipo de referência como anulável:

  • Melhor análise de fluxo estático que determina se uma variável pode ser null antes de desreferencá-la.
  • Atributos que anotam APIs para que a análise de fluxo determine o estado nulo.
  • Anotações variáveis que os desenvolvedores usam para declarar explicitamente o estado nulo pretendido para uma variável.

A análise de estado nulo e as anotações variáveis são desabilitadas por padrão para projetos existentes, o que significa que todos os tipos de referência continuam a ser anuláveis. A partir do .NET 6, eles são habilitados por padrão para novos projetos. Para obter informações sobre como habilitar esses recursos declarando um contexto de anotação anulável, consulte contextos anuláveis.

O restante deste artigo descreve como essas três áreas de recursos funcionam para produzir avisos quando seu código pode estar desreferenciando um null valor. Desreferenciar uma variável significa acessar um de seus membros usando o . operador (ponto), conforme mostrado no exemplo a seguir:

string message = "Hello, World!";
int length = message.Length; // dereferencing "message"

Quando você desreferencia uma variável cujo valor é null, o runtime lança um System.NullReferenceException.

Você também pode explorar esses conceitos em nosso módulo de aprendizado sobre segurança anulável em C#.

Análise de estado nulo

A análise de estadonulo rastreia o estado nulo das referências. Essa análise estática emite avisos quando o código pode ser desreferenciado null. Você pode abordar esses avisos para minimizar as incidências quando o runtime gerar um System.NullReferenceException. O compilador usa a análise estática para determinar o estado nulo de uma variável. Uma variável não é nula ou talvez nula. O compilador determina que uma variável não é nula de duas maneiras:

  1. A variável recebeu um valor que é conhecido por não ser nulo.
  2. A variável foi verificada null e não foi modificada desde essa verificação.

Qualquer variável que o compilador não tenha determinado como não nula é considerada talvez nula. A análise fornece avisos em situações em que você pode acidentalmente desreferenciar um null valor. O compilador produz avisos com base no estado nulo.

  • Quando uma variável não é nula, essa variável pode ser desreferenciada com segurança.
  • Quando uma variável é talvez nula, essa variável deve ser verificada para garantir que não null seja antes de desreferencá-la.

Considere o exemplo a seguir:

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

No exemplo anterior, o compilador determina que messagetalvez seja nulo quando a primeira mensagem é impressa. Não há nenhum aviso para a segunda mensagem. A linha final do código produz um aviso porque originalMessage pode ser nulo. O exemplo a seguir mostra um uso mais prático para percorrer uma árvore de nós até a raiz, processando cada nó durante a passagem:

void FindRoot(Node node, Action<Node> processNode)
{
    for (var current = node; current != null; current = current.Parent)
    {
        processNode(current);
    }
}

O código anterior não gera avisos para desreferenciar a variável current. A análise estática determina que current isso nunca é desreferenciado quando talvez seja nulo. A variável current é verificada null antes current.Parent de ser acessada e antes de passar current para a ação ProcessNode . Os exemplos anteriores mostram como o compilador determina o estado nulo para variáveis locais quando inicializado, atribuído ou comparado a null.

A análise de estado nulo não rastreia os métodos chamados. Como resultado, os campos inicializados em um método auxiliar comum chamado pelos construtores gerarão um aviso com o seguinte modelo:

A propriedade "name" não anulável deve conter um valor não nulo ao sair do construtor.

Você pode abordar esses avisos de duas maneiras: encadeamento de construtor ou atributos anuláveis no método auxiliar. O código a seguir mostra um exemplo de cada um desses casos. A Person classe usa um construtor comum chamado por todos os outros construtores. A Student classe tem um método auxiliar anotado com o System.Diagnostics.CodeAnalysis.MemberNotNullAttribute atributo:


using System.Diagnostics.CodeAnalysis;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public Person() : this("John", "Doe") { }
}

public class Student : Person
{
    public string Major { get; set; }

    public Student(string firstName, string lastName, string major)
        : base(firstName, lastName)
    {
        SetMajor(major);
    }

    public Student(string firstName, string lastName) :
        base(firstName, lastName)
    {
        SetMajor();
    }

    public Student()
    {
        SetMajor();
    }

    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = default)
    {
        Major = major ?? "Undeclared";
    }
}

Observação

Várias melhorias na atribuição definitiva e na análise de estado nulo foram adicionadas no C# 10. Ao atualizar para o C# 10, você poderá encontrar menos avisos anuláveis que são falsos positivos. Você pode saber mais sobre os aprimoramentos na especificação de recursos para aprimoramentos de atribuição definidos.

A análise de estado anulável e os avisos gerados pelo compilador ajudam você a evitar erros de programa desreferenciando null. O artigo sobre como resolver avisos anuláveis fornece técnicas para corrigir os avisos que você provavelmente verá em seu código.

Atributos em assinaturas de API

A análise de estado nulo precisa de dicas dos desenvolvedores para entender a semântica das APIs. Algumas APIs fornecem verificações nulas e devem alterar o estado nulo de uma variável de talvez nulo para não nulo. Outras APIs retornam expressões que não são nulas ou talvez nulas , dependendo do estado nulo dos argumentos de entrada. Por exemplo, considere o seguinte código que exibe uma mensagem:

public void PrintMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

Com base na inspeção, qualquer desenvolvedor consideraria esse código seguro e não deveria gerar avisos. O compilador não sabe que IsNullOrWhiteSpace fornece uma verificação nula. Você aplica atributos para informar o compilador que messagenão é nulo se e somente se IsNullOrWhiteSpace retornar false. No exemplo anterior, a assinatura inclui o NotNullWhen estado nulo de message:

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

Os atributos fornecem informações detalhadas sobre o estado nulo de argumentos, valores retornados e membros da instância de objeto usada para invocar um membro. Os detalhes sobre cada atributo podem ser encontrados no artigo de referência de linguagem sobre atributos de referência anuláveis. Todas as APIs de runtime do .NET foram anotadas no .NET 5. Você melhora a análise estática anotando suas APIs para fornecer informações semânticas sobre o estado nulo de argumentos e valores retornados.

Anotações de variáveis anuláveis

A análise de estado nulo fornece uma análise robusta para a maioria das variáveis. O compilador precisa de mais informações de você para variáveis de membro. O compilador não pode fazer suposições sobre a ordem na qual os membros públicos são acessados. Qualquer membro público pode ser acessado em qualquer ordem. Qualquer um dos construtores acessíveis pode ser usado para inicializar o objeto. Se um campo membro pode ser definido como null, o compilador deve assumir que seu estado nuloé talvez nulo no início de cada método.

Você usa anotações que podem declarar se uma variável é um tipo de referência anulável ou um tipo de referência não anulável. Essas anotações fazem instruções importantes sobre o estado nulo para variáveis:

  • Uma referência não deve ser nula. O estado padrão de uma variável de referência não nula é não nulo. O compilador impõe regras que garantem que seja seguro desreferenciar essas variáveis sem antes verificar se elas não são nulas:
    • A variável deve ser inicializada para um valor não nulo.
    • A variável nunca pode receber o valor null. O compilador emite um aviso quando o código atribui uma expressão talvez nula a uma variável que não deve ser nula.
  • Uma referência pode ser nula. O estado padrão de uma variável de referência anulável é talvez nulo. O compilador impõe regras para garantir que você tenha verificado corretamente uma null referência:
    • A variável só pode ser desreferenciada quando o compilador pode garantir que o valor não nullseja .
    • Essas variáveis podem ser inicializadas com o valor null padrão e receber o valor null em outro código.
    • O compilador não emite avisos quando o código atribui uma expressão talvez nula a uma variável que pode ser nula.

Qualquer variável de referência que não deveria ser null tem um estado nulode não nulo. Qualquer variável de referência que possa ser null inicialmente tem o estado nulo de talvez nulo.

Um tipo de referência que permite valor nulo é indicado usando a mesma sintaxe que tipos de valor que permitem valor nulo: um ? é acrescentado ao tipo da variável. Por exemplo, a seguinte declaração de variável representa uma variável de cadeia de caracteres que permite valor nulo, name:

string? name;

Qualquer variável em ? que não seja acrescentada ao nome do tipo é um tipo de referência não anulável. Isso inclui todas as variáveis de tipo de referência no código existente quando você habilitou esse recurso. No entanto, todas as variáveis locais tipada implicitamente (declaradas usando var) são tipos de referência anuláveis. Como as seções anteriores mostraram, a análise estática determina o estado nulo das variáveis locais para determinar se elas talvez sejam nulas.

Às vezes, você deve substituir um aviso quando souber que uma variável não é nula, mas o compilador determina que seu estado nulo é talvez nulo. Você usa o operador que permite perdão nulo seguindo um nome de variável para forçar o estado nulo a ser não nulo.! Por exemplo, se você souber que a name variável não null está, mas o compilador emite um aviso, você poderá escrever o seguinte código para substituir a análise do compilador:

name!.Length;

Tipos de referência anuláveis e tipos de valor anuláveis fornecem um conceito semântico semelhante: uma variável pode representar um valor ou objeto ou essa variável pode ser null. No entanto, tipos de referência anuláveis e tipos de valor anuláveis são implementados de forma diferente: tipos de valor anuláveis são implementados usando System.Nullable<T>e tipos de referência anuláveis são implementados por atributos lidos pelo compilador. Por exemplo, string? e string ambos são representados pelo mesmo tipo: System.String. No entanto, int? e int são representados por System.Nullable<System.Int32> e System.Int32, respectivamente.

Tipos de referência anuláveis são um recurso de tempo de compilação. Isso significa que é possível que os chamadores ignorem avisos, usem null intencionalmente como um argumento para um método que espera uma referência não anulável. Os autores da biblioteca devem incluir verificações de runtime em relação a valores de argumento nulos. A ArgumentNullException.ThrowIfNull opção preferencial para verificar um parâmetro em relação ao nulo no tempo de execução.

Importante

Habilitar anotações anuláveis pode alterar a forma como o Entity Framework Core determina se um membro de dados é necessário. Você pode saber mais detalhes no artigo sobre os conceitos básicos do Entity Framework Core: Trabalhando com tipos de referência anuláveis.

Genéricos

Os genéricos exigem regras detalhadas para lidar com T? qualquer parâmetro Tde tipo. As regras são necessariamente detalhadas devido ao histórico e à implementação diferente para um tipo de valor anulável e um tipo de referência anulável. Tipos de valor anuláveis são implementados usando o System.Nullable<T> struct. Tipos de referência anuláveis são implementados como anotações de tipo que fornecem regras semânticas para o compilador.

No C# 8.0, usar T? sem restrição T para ser um struct ou um class não compilado. Isso permitiu que o compilador interpretasse T? claramente. Essa restrição foi removida no C# 9.0, definindo as seguintes regras para um parâmetro Tde tipo não treinado:

  • Se o argumento T de tipo for um tipo de referência, T? referenciará o tipo de referência anulável correspondente. Por exemplo, se T for um string, então T? será um string?.
  • Se o argumento de tipo for T um tipo de valor, T? faça referência ao mesmo tipo de valor. T Por exemplo, se T for um int, o T? também será um int.
  • Se o argumento de tipo for T um tipo de referência anulável, T? referenciará esse mesmo tipo de referência anulável. Por exemplo, se T for um string?, também T? será um string?.
  • Se o argumento de tipo for T um tipo de valor anulável, T? referenciará esse mesmo tipo de valor anulável. Por exemplo, se T for um int?, também T? será um int?.

Para valores retornados, T? é equivalente a [MaybeNull]T; para valores de argumento, T? é equivalente a [AllowNull]T. Para obter mais informações, consulte o artigo sobre Atributos para análise de estado nulo na referência de linguagem.

Você pode especificar um comportamento diferente usando restrições:

  • A class restrição significa que T deve ser um tipo de referência não anulável (por exemplo string). O compilador produzirá um aviso se você usar um tipo de referência anulável, como string? para T.
  • A class? restrição significa que T deve ser um tipo de referência, não anulável (string) ou um tipo de referência anulável (por exemplo string?). Quando o parâmetro de tipo é um tipo de referência anulável, como string?, uma expressão de referências que o mesmo tipo de T? referência anulável, como string?.
  • A notnull restrição significa que T deve ser um tipo de referência não anulável ou um tipo de valor não anulável. Se você usar um tipo de referência anulável ou um tipo de valor anulável para o parâmetro de tipo, o compilador produzirá um aviso. Além disso, quando T é um tipo de valor, o valor retornado é esse tipo de valor, não o tipo de valor anulável correspondente.

Essas restrições ajudam a fornecer mais informações ao compilador sobre como T serão usadas. Isso ajuda quando os desenvolvedores escolhem o tipo e Tfornece uma melhor análise de estado nulo quando uma instância do tipo genérico é usada.

Contextos que permitem valor nulo

Os novos recursos que protegem contra o lançamento de um System.NullReferenceException podem ser disruptivos quando ativados em uma base de código existente:

  • Todas as variáveis de referência tipadas explicitamente são interpretadas como tipos de referência não anuláveis.
  • O significado da class restrição em genéricos foi alterado para significar um tipo de referência não anulável.
  • Novos avisos são gerados devido a essas novas regras.

Você deve aceitar explicitamente o uso desses recursos em seus projetos existentes. Isso fornece um caminho de migração e preserva a compatibilidade com versões anteriores. Contextos que permitem valor nulo habilitam o controle refinado para a maneira como o compilador interpreta variáveis de tipo de referência. O contexto de anotação anulável determina o comportamento do compilador. Há quatro valores para o contexto de anotação anulável:

  • disable: O compilador se comporta de forma semelhante ao C# 7.3 e anterior:
    • Avisos anuláveis estão desabilitados.
    • Todas as variáveis de tipo de referência são tipos de referência anuláveis.
    • Você não pode declarar uma variável como um tipo de referência anulável usando o ? sufixo no tipo.
    • Você pode usar o operador de perdão nulo, !mas ele não tem efeito.
  • habilitar: o compilador habilita todas as análises de referência nulas e todos os recursos de linguagem.
    • Todos os novos avisos anuláveis estão habilitados.
    • Você pode usar o ? sufixo para declarar um tipo de referência anulável.
    • Todas as outras variáveis de tipo de referência são tipos de referência não anuláveis.
    • O operador de perdão nulo suprime avisos para uma possível atribuição para null.
  • avisos: o compilador executa todas as análises nulas e emite avisos quando o código pode desreferenciar null.
    • Todos os novos avisos anuláveis estão habilitados.
    • O uso do ? sufixo para declarar um tipo de referência anulável produz um aviso.
    • Todas as variáveis de tipo de referência têm permissão para serem nulas. No entanto, os membros têm o estado nulo de não nulo na chave de abertura de todos os métodos, a menos que declarados com o ? sufixo.
    • Você pode usar o operador de perdão nulo, !.
  • anotações: o compilador não executa a análise nula ou emite avisos quando o código pode desreferenciar null.
    • Todos os novos avisos anuláveis estão desabilitados.
    • Você pode usar o ? sufixo para declarar um tipo de referência anulável.
    • Todas as outras variáveis de tipo de referência são tipos de referência não anuláveis.
    • Você pode usar o operador de perdão nulo, !mas ele não tem efeito.

O contexto de anotação anulável e o contexto de aviso anulável podem ser definidos para um projeto usando o <Nullable> elemento em seu arquivo .csproj . Esse elemento configura como o compilador interpreta a nulidade dos tipos e quais avisos são emitidos. A tabela a seguir mostra os valores permitidos e resume os contextos que eles especificam.

Contexto Avisos de desreferência Avisos de atribuição Tipos de referência ? Sufixo Operador !
disable Desabilitado Desabilitado Todos são anuláveis Não é possível usar Não tem efeito
enable habilitado habilitado Não anulável, a menos que declarado com ? Declara tipo anulável Suprime avisos para uma possível null atribuição
warnings Habilitada Não aplicável Todos são anuláveis, mas os membros não são considerados nulos na abertura da chave de métodos Produz um aviso Suprime avisos para uma possível null atribuição
annotations Desabilitado Desabilitado Não anulável, a menos que declarado com ? Declara tipo anulável Não tem efeito

As variáveis de tipo de referência no código compilado antes do C# 8 ou em um contexto desabilitado são anuláveis. Você pode atribuir uma null variável literal ou talvez nula a uma variável que seja alheia anulável. No entanto, o estado padrão de uma variável alheia anulávelnão é nulo.

Você pode escolher qual configuração é melhor para seu projeto:

  • Escolha desabilitar projetos herdados que você não deseja atualizar com base no diagnóstico ou em novos recursos.
  • Escolha avisos para determinar onde seu código pode gerar System.NullReferenceExceptions. Você pode resolver esses avisos antes de modificar o código para habilitar tipos de referência não anuláveis.
  • Escolha anotações para expressar sua intenção de design antes de habilitar avisos.
  • Escolha habilitar para novos projetos e projetos ativos em que você deseja proteger contra exceções de referência nulas.

Exemplo:

<Nullable>enable</Nullable>

Você também pode usar diretivas para definir esses mesmos contextos em qualquer lugar no código-fonte. Eles são mais úteis quando você está migrando uma base de código grande.

  • #nullable enable: define o contexto de anotação anulável e o contexto de aviso anulável a ser habilitado.
  • #nullable disable: define o contexto de anotação anulável e o contexto de aviso anulável para desabilitar.
  • #nullable restore: restaura o contexto de anotação anulável e o contexto de aviso anulável para as configurações do projeto.
  • #nullable disable warnings: defina o contexto de aviso anulável para desabilitar.
  • #nullable enable warnings: defina o contexto de aviso anulável para habilitar.
  • #nullable restore warnings: restaura o contexto de aviso anulável para as configurações do projeto.
  • #nullable disable annotations: defina o contexto de anotação anulável para desabilitar.
  • #nullable enable annotations: defina o contexto de anotação anulável a ser habilitado.
  • #nullable restore annotations: restaura o contexto de aviso de anotação para as configurações do projeto.

Para qualquer linha de código, você pode definir qualquer uma das seguintes combinações:

Contexto de aviso Contexto de anotação Use
padrão do projeto padrão do projeto Padrão
enable disable Corrigir avisos de análise
enable padrão do projeto Corrigir avisos de análise
padrão do projeto enable Adicionar anotações de tipo
enable enable Código já migrado
disable enable Anotar código antes de corrigir avisos
disable disable Adicionando código herdado ao projeto migrado
padrão do projeto disable Raramente
disable padrão do projeto Raramente

Essas nove combinações fornecem controle refinado sobre os diagnósticos que o compilador emite para seu código. Você pode habilitar mais recursos em qualquer área que estiver atualizando, sem ver avisos adicionais que ainda não está pronto para resolver.

Importante

O contexto global anulável não se aplica a arquivos de código gerados. Em qualquer estratégia, o contexto anulável é desabilitado para qualquer arquivo de origem marcado como gerado. Isso significa que todas as APIs em arquivos gerados não são anotadas. Há quatro maneiras de um arquivo ser marcado como gerado:

  1. No .editorconfig, especifique generated_code = true em uma seção que se aplica a esse arquivo.
  2. Coloque <auto-generated> ou <auto-generated/> comente na parte superior do arquivo. Ele pode estar em qualquer linha nesse comentário, mas o bloco de comentários deve ser o primeiro elemento no arquivo.
  3. Iniciar o nome do arquivo com TemporaryGeneratedFile_
  4. Termine o nome do arquivo com .designer.cs, .generated.cs, .g.cs ou .g.i.cs.

Os geradores podem aceitar o uso da #nullable diretiva de pré-processador.

Por padrão, as anotações anuláveis e os contextos de aviso são desabilitados. Isso significa que o código existente é compilado sem alterações e sem gerar novos avisos. A partir do .NET 6, novos projetos incluem o <Nullable>enable</Nullable> elemento em todos os modelos de projeto.

Essas opções fornecem duas estratégias distintas para atualizar uma base de código existente para usar tipos de referência anuláveis.

Armadilhas conhecidas

Matrizes e structs que contêm tipos de referência são armadilhas conhecidas em referências anuláveis e a análise estática que determina a segurança nula. Em ambas as situações, uma referência não anulável pode ser inicializada nullsem gerar avisos.

Estruturas

Um struct que contém tipos de referência não anuláveis permite atribuir default para ele sem avisos. Considere o exemplo a seguir:

using System;

#nullable enable

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static class Program
{
    public static void PrintStudent(Student student)
    {
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
    }

    public static void Main() => PrintStudent(default);
}

No exemplo anterior, não há nenhum aviso enquanto PrintStudent(default) os tipos FirstName de referência não anuláveis e LastName são nulos.

Outro caso mais comum é quando você lida com structs genéricos. Considere o exemplo a seguir:

#nullable enable

public struct Foo<T>
{
    public T Bar { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string s = default(Foo<string>).Bar;
    }
}

No exemplo anterior, a propriedade Bar será null em tempo de execução e será atribuída a uma cadeia de caracteres não anulável sem avisos.

Matrizes

Matrizes também são uma armadilha conhecida em tipos de referência anuláveis. Considere o exemplo a seguir que não produz avisos:

using System;

#nullable enable

public static class Program
{
    public static void Main()
    {
        string[] values = new string[10];
        string s = values[0];
        Console.WriteLine(s.ToUpper());
    }
}

No exemplo anterior, a declaração da matriz mostra que ela contém cadeias de caracteres não anuláveis, enquanto seus elementos são inicializados para null. Em seguida, a variável s recebe um null valor (o primeiro elemento da matriz). Por fim, a variável s é desreferenciada causando uma exceção de runtime.

Confira também