Tipos de referência anulável (referência do C#)

Observação

Este artigo aborda os tipos de referência anulável. Você também pode declarar os tipos de valor anulável.

Use tipos de referência anuláveis no código que estão em um contexto de reconhecimento anulável. Os tipos de referência anulável, os avisos de análise estática nula e o operador tolerante a nulo são recursos de linguagem opcionais. Todos estão desativados por padrão. Você controla um contexto anulável no nível do projeto usando configurações de build ou em código usando pragmas.

A linguagem C# faz referência a documentos da versão mais recentemente lançada da linguagem C#. Ele também contém a documentação inicial para recursos em visualizações públicas para a próxima versão do idioma.

A documentação identifica qualquer recurso introduzido pela primeira vez nas três últimas versões do idioma ou nas versões prévias públicas atuais.

Dica

Para descobrir quando um recurso foi introduzido pela primeira vez em C#, consulte o artigo sobre o histórico de versão da linguagem C#.

Importante

Todos os modelos de projeto habilitam o contexto anulável para o projeto. Os projetos criados com modelos anteriores não incluem esse elemento, e esses recursos ficam desativados até que você os habilite no arquivo de projeto ou use pragmas.

Em um contexto com reconhecimento anulável:

  • Você deve inicializar uma variável de um tipo T de referência com um valor não nulo e nunca pode atribuir um valor que possa ser null.
  • Você pode inicializar uma variável de um tipo T? de referência com null ou atribuir null, mas você deve verificá-la antes null de desreferenciar.
  • Quando você aplica o operador de perdão nulo a uma variável m do tipo T?, como em m!, a variável é considerada não nula.

O compilador impõe as distinções entre um tipo T de referência não anulável e um tipo T? de referência anulável usando as regras anteriores. Uma variável do tipo T e uma variável do tipo T? são do mesmo tipo .NET. O exemplo a seguir declara uma cadeia de caracteres não anulável e uma cadeia de caracteres anulável e, em seguida, usa o operador tolerante a nulo para atribuir um valor a uma cadeia de caracteres não anulável:

string notNull = "Hello";
string? nullable = default;
notNull = nullable!; // null forgiveness

As variáveis notNull e nullable ambos usam o String tipo. Como os tipos não anuláveis e anuláveis usam o mesmo tipo, você não pode usar um tipo de referência anulável em vários locais. Em geral, você não pode usar um tipo de referência anulável como uma classe base ou uma interface implementada. Você não pode usar um tipo de referência anulável em qualquer expressão de criação de objeto ou teste de tipo. Você não pode usar um tipo de referência anulável como o tipo de expressão de acesso de membro. Os exemplos a seguir mostram esses constructos:

public MyClass : System.Object? // not allowed
{
}

var nullEmpty = System.String?.Empty; // Not allowed
var maybeObject = new object?(); // Not allowed
try
{
    if (thing is string? nullableString) // not allowed
        Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
    Console.WriteLine("error");
}

Referências anuláveis e análise estática

Os exemplos na seção anterior ilustram a natureza dos tipos de referência anulável. Os tipos de referência anulável não são novos tipos de classe, mas sim anotações sobre os tipos de referência existentes. O compilador usa essas anotações para ajudar a encontrar possíveis erros de referência nula no código. Não há diferença de runtime entre um tipo de referência não anulável e um tipo de referência anulável. O compilador não adiciona verificações de runtime para tipos de referência não anulável. Os benefícios estão na análise do tempo de compilação. O compilador gera avisos que ajudam a localizar e corrigir possíveis erros nulos no código. Você declara a intenção e o compilador avisa quando o código violar essa intenção.

Importante

Anotações de referência anuláveis não introduzem alterações de comportamento, mas outras bibliotecas podem usar reflexão para produzir diferentes comportamentos de runtime para tipos de referência anuláveis e não anuláveis. Notavelmente, o Entity Framework Core lê atributos anuláveis. Ele interpreta uma referência anulável como um valor opcional e uma referência não anulável como um valor necessário.

Em um contexto habilitado para anulável, o compilador executa uma análise estática nas variáveis de qualquer tipo de referência, anulável e não anulável. O compilador rastreia o estado nulo de cada variável de referência como não nulo ou talvez nulo. O estado padrão de uma referência não anulável é não anulável. O estado padrão de uma variável de referência anulável é talvez nulo.

Os tipos de referência não anulável devem estar sempre seguros para desreferenciar, pois o estado nulo é não nulo. Para impor essa regra, o compilador emitirá avisos se um tipo de referência não anulável não for inicializado para um valor não nulo. Você deve atribuir variáveis locais em que as declare. Cada campo deve receber um valor não nulo, em um inicializador de campo ou em cada construtor. O compilador emite avisos quando uma referência não anulável é atribuída a uma referência cujo estado é talvez nulo. Geralmente, uma referência não anulável não é nula e nenhum aviso é emitido quando você desreferencia essas variáveis.

Observação

Se você atribuir uma expressão talvez nula a um tipo de referência não anulável, o compilador gerará um aviso. O compilador gera avisos para essa variável até que seja atribuída a uma expressão não nula.

Você pode inicializar ou atribuir null a tipos de referência anuláveis. Portanto, a análise estática deve determinar se uma variável é não nula, antes que seja desreferenciada. Se uma referência anulável for determinada como talvez nula, a atribuição a uma variável de referência não anulável gerará um aviso do compilador. A classe a seguir mostra exemplos desses avisos:

public class ProductDescription
{
    private string shortDescription;
    private string? detailedDescription;

    public ProductDescription() // Warning! shortDescription not initialized.
    {
    }

    public ProductDescription(string productDescription) =>
        this.shortDescription = productDescription;

    public void SetDescriptions(string productDescription, string? details=null)
    {
        shortDescription = productDescription;
        detailedDescription = details;
    }

    public string GetDescription()
    {
        if (detailedDescription.Length == 0) // Warning! dereference possible null
        {
            return shortDescription;
        }
        else
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
    }

    public string FullDescription()
    {
        if (detailedDescription == null)
        {
            return shortDescription;
        }
        else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
        return shortDescription;
    }
}

O snippet a seguir mostra onde o compilador emite avisos ao usar essa classe:

string shortDescription = default; // Warning! non-nullable set to null;
var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription maybe null.

string description = "widget";
var item = new ProductDescription(description);

item.SetDescriptions(description, "These widgets will do everything.");

Os exemplos anteriores demonstram como a análise estática do compilador determina o estado nulo das variáveis de referência. O compilador aplica regras de linguagem para verificações e atribuições nulas para informar a análise. O compilador não pode fazer suposições sobre a semântica de métodos ou propriedades. Se você chamar métodos que executam verificações nulas, o compilador não poderá saber se esses métodos afetam o estado nulo de uma variável. Você pode adicionar atributos às SUAS APIs para informar o compilador sobre a semântica de argumentos e valores retornados. Muitas APIs comuns nas bibliotecas .NET têm esses atributos. Por exemplo, o compilador interpreta corretamente IsNullOrEmpty como uma verificação nula. Para obter mais informações sobre os atributos que se aplicam à análise estática de estado nulo, confira o artigo sobre Atributos anuláveis.

Contexto que permite valor nulo

O contexto anulável determina como o compilador lida com anotações de tipo de referência anuláveis e quais avisos ele produz durante a análise de estado nulo estático. O contexto anulável contém dois sinalizadores: a configuração de anotação e a configuração de aviso.

As configurações de anotação e aviso são desabilitadas por padrão para projetos existentes. A partir do .NET 6 (C# 10), os dois sinalizadores são habilitados por padrão para novos projetos. O motivo para dois sinalizadores distintos para o contexto anulável é facilitar a migração de grandes projetos que são anteriores à introdução de tipos de referência anuláveis.

Em projetos pequenos, você pode habilitar tipos de referência anuláveis, corrigir avisos e continuar. No entanto, para projetos maiores e soluções de vários projetos, esse processo pode gerar um grande número de avisos. Você pode usar pragmas para habilitar tipos de referência anuláveis arquivo por arquivo à medida que começar a usar os tipos de referência anuláveis. 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 restrição class 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.

O contexto de anotação anulável determina o comportamento do compilador. Há quatro combinações para as configurações de contexto anulável.

  • ambos desabilitados: o código é nullable-oblivious. Desabilitar corresponde ao comportamento anterior à habilitação dos tipos de referência anuláveis, exceto pelo fato de que a nova sintaxe produz avisos em vez de erros.
    • Avisos anuláveis estão desabilitados.
    • Todas as variáveis de tipo de referência são tipos de referência anuláveis.
    • O uso do sufixo ? para declarar um tipo de referência anulável produz um aviso.
    • Você pode usar o operador tolerante a nulo, !, mas ele não tem efeito.
  • ambos habilitados: o compilador habilita toda a análise de referência nula 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.
    • As variáveis de tipo de referência sem o sufixo ? são tipos de referência não anuláveis.
    • O operador de tolerância a nulo suprime avisos para uma possível desreferência a null.
  • aviso habilitado: 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 null-state de not-null na chave de abertura de todos os métodos, a menos que declarados com o sufixo ?.
    • Você pode usar o operador de tolerância a nulo, !.
  • anotações habilitadas: o compilador não gera avisos quando o código pode desreferenciar null, ou quando você atribui uma expressão possivelmente nula a uma variável não anulável.
    • Todos os avisos anuláveis estão desabilitados.
    • Você pode usar o sufixo ? para declarar um tipo de referência anulável.
    • As variáveis de tipo de referência sem o sufixo ? são tipos de referência não anuláveis.
    • Você pode usar o operador tolerante a nulo, !, mas ele não tem efeito.

Você pode definir o contexto de anotação anulável e o contexto de aviso anulável para um projeto usando o <Nullable> elemento em seu arquivo .csproj . Esse elemento configura como o compilador interpreta a nulidade de tipos e quais avisos ele emite. 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ências sufixo ? operador !
disable Desactivado Desactivado Todos são anuláveis Produz um aviso 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 atribuição null
warnings Habilitado Não aplicável Todos são anuláveis, mas os membros são considerados não nulos na abertura dos métodos Produz um aviso Suprime avisos para uma possível atribuição null
annotations Desactivado Desactivado 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 em um contexto disabled são nullable-oblivious. Você pode atribuir um literal null ou uma variável possivelmente nula para uma variável que seja alheia anulável. No entanto, o estado padrão de uma variável nullable-oblivious é not-null.

Escolha a configuração que melhor se ajusta ao seu projeto:

  • Escolha disable em projetos herdados que você não deseja atualizar com base no diagnóstico ou em novos recursos.
  • Escolha avisos para determinar o local em que seu código poderá 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 annotations para expressar sua intenção de design antes de habilitar avisos.
  • Escolha enable 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 sinalizadores em qualquer lugar no código-fonte. Essas diretivas são mais úteis quando você está migrando uma grande base de código.

  • #nullable enable: define os sinalizadores de anotação e aviso como habilitar.
  • #nullable disable: define os sinalizadores de anotação e aviso como desabilitar.
  • #nullable restore: restaura o sinalizador de anotação e o sinalizador de aviso para as configurações do projeto.
  • #nullable disable warnings: define o sinalizador de aviso para desabilitar.
  • #nullable enable warnings: define o sinalizador de aviso a ser habilitado.
  • #nullable restore warnings: restaura o sinalizador de aviso para as configurações do projeto.
  • #nullable disable annotations: define o sinalizador de anotação a ser desabilitado.
  • #nullable enable annotations: define o sinalizador de anotação a ser habilitado.
  • #nullable restore annotations: restaura o sinalizador de anotação para as configurações do projeto.

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

Sinalizador de aviso Sinalizador de anotação Utilização
Padrão do projeto Padrão do projeto Default
ativar desabilitar Corrigir avisos da análise
ativar Padrão do projeto Corrigir avisos da análise
Padrão do projeto ativar Adicionar anotações de tipo
ativar ativar Código já migrado
desabilitar ativar Anotar código antes de corrigir avisos
desabilitar desabilitar Adicionando código herdado ao projeto migrado
Padrão do projeto desabilitar Raramente
desabilitar Padrão do projeto Raramente

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

Importante

O contexto global anulável não se aplica aos arquivos de código gerados. Em qualquer das estratégias, o contexto anulável é desabilitado para todo arquivo de origem marcado como gerado. Essa condição significa que o compilador não anota nenhuma APIs em arquivos gerados. O compilador não produz avisos anuláveis para arquivos gerados. Um arquivo é marcado como gerado de qualquer uma das quatro maneiras a seguir:

  1. No .editorconfig, especifique generated_code = true em uma seção que se aplica a esse arquivo.
  2. Coloque <auto-generated> ou <auto-generated/> em um comentário 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 do arquivo.
  3. Inicie 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 usando a #nullable diretiva de pré-processador.

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

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

Definição de contexto anulável

Você pode controlar o contexto anulável de duas maneiras. No nível do projeto, adicione a configuração do <Nullable>enable</Nullable> projeto. Em um único arquivo de origem C#, adicione o #nullable enable pragma para habilitar o contexto anulável. Para obter mais informações, consulte como definir uma estratégia anulável. Antes do .NET 6, novos projetos usam o padrão, <Nullable>disable</Nullable>. A partir do .NET 6, os novos projetos incluem o elemento <Nullable>enable</Nullable> no arquivo de projeto.

Genéricos

Quando você usa um parâmetro de tipo, Tcomo seu equivalente anulável, T?o argumento de tipo real determina como o ? valor é interpretado. Considere a seguinte declaração genérica:

public class Box<T>
{
    public T Contents { get; set; }
}

Como um parâmetro de tipo pode ser usado para um tipo de referência ou um tipo de valor, o significado depende de qual argumento de T? tipo o chamador fornece. As regras a seguir descrevem o que T? resolve quando T não tem restrições:

  • O argumento de tipo é um tipo de referência não anulável. For Box<string>, T is string and T? is string?— the corresponding nullable reference type.
  • O argumento de tipo é um tipo de valor. For Box<int>, T is int and T? is also int— the same value type. A anotação não tem efeito sobre tipos de valor, a menos que o parâmetro de tipo tenha a struct restrição, nesse caso T? significa Nullable<T> (int?).
  • O argumento de tipo já é anulável. Pois Box<string?>, T é string? e T? ainda string?é . Você não obtém um tipo "duplamente anulável".

Restrições restringem quais argumentos de tipo são permitidos. Eles também permitem que o compilador raciocinar sobre como T pode ser usado:

  • where T : class requer um tipo de referência não anulável. Box<string> é permitido; Box<string?> produz um aviso.
  • where T : class? permite um tipo de referência anulável ou não anulável. Ambos Box<string> e Box<string?> são permitidos.
  • where T : struct requer um tipo de valor não anulável. Box<int> é permitido; Box<int?> Não. Com essa restrição, T? dentro dos meios Nullable<T>genéricos — para Box<int>, T? é int?.
  • where T : notnull requer um tipo de valor ou referência não anulável. Box<string> e Box<int> são permitidos; Box<string?> produz um aviso.
  • where T : BaseType requer um tipo de referência não anulável que deriva de BaseType. Acrescente ? (where T : BaseType?) para permitir tipos derivados anuláveis também.

As restrições ajudam o compilador a raciocinar sobre como um parâmetro de tipo genérico é usado:

public static T? FirstOrDefault<T>(IEnumerable<T> source)
{
    foreach (T item in source)
    {
        return item;
    }
    return default;
}

public static void RequireNotNull<T>(T value) where T : notnull
{
    ArgumentNullException.ThrowIfNull(value);
}

public static void Generics()
{
    string? first = FirstOrDefault<string>([]);
    Console.WriteLine(first ?? "<empty>");

    RequireNotNull("not null");
}

Especificação da linguagem C#

Para obter mais informações, consulte a seção Tipos de referência anuláveis da especificação da linguagem C#.

Veja também