Atributos para análise estática de estado nulo interpretados pelo compilador C#

Em um contexto habilitado anulável, o compilador executa a análise estática do código para determinar o estado nulo de todas as variáveis de tipo de referência:

  • not-null: A análise estática determina que uma variável tem um valor não nulo.
  • maybe-null: A análise estática não pode determinar que uma variável recebe um valor não nulo.

Esses estados permitem que o compilador forneça avisos quando você pode cancelar a referência de um valor nulo, lançando um System.NullReferenceExceptionarquivo . Esses atributos fornecem ao compilador informações semânticas sobre o estado nulo de argumentos, valores de retorno e membros de objeto com base no estado de argumentos e valores de retorno. O compilador fornece avisos mais precisos quando suas APIs foram anotadas corretamente com essas informações semânticas.

Este artigo fornece uma breve descrição de cada um dos atributos de tipo de referência anuláveis e como usá-los.

Comecemos por um exemplo. Imagine que sua biblioteca tem a seguinte API para recuperar uma cadeia de caracteres de recurso. Este método foi originalmente compilado em um contexto anulável esquecido :

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

O exemplo anterior segue o padrão familiar Try* no .NET. Há dois parâmetros de referência para essa API: o key e o message. Essa API tem as seguintes regras relacionadas ao estado nulo desses parâmetros:

  • Os chamadores não devem passar null como argumento para key.
  • Os chamadores podem passar uma variável cujo valor é null como argumento para message.
  • Se o TryGetMessage método retornar true, o valor de message não é nulo. Se o valor de retorno for false, o valor de message é null.

A regra para key pode ser expressa de forma sucinta: key deve ser um tipo de referência não anulável. O message parâmetro é mais complexo. Permite uma variável que é null como o argumento, mas garante, no sucesso, que o out argumento não nullé. Para esses cenários, você precisa de um vocabulário mais rico para descrever as expectativas. O NotNullWhen atributo, descrito abaixo, descreve o estado nulo para o argumento usado para o message parâmetro.

Nota

Adicionar esses atributos dá ao compilador mais informações sobre as regras para sua API. Quando o código de chamada é compilado em um contexto habilitado anulável, o compilador avisará os chamadores quando eles violarem essas regras. Esses atributos não permitem mais verificações em sua implementação.

Atributo Categoria Significado
AllowNull Pré-condição Um parâmetro, campo ou propriedade não anulável pode ser nulo.
DisallowNull Pré-condição Um parâmetro, campo ou propriedade anulável nunca deve ser nulo.
TalvezNulo Pós-condição Um parâmetro, campo, propriedade ou valor de retorno não anulável pode ser nulo.
NotNull Pós-condição Um parâmetro, campo, propriedade ou valor de retorno anulável nunca será nulo.
MaybeNullWhen Pós-condição condicional Um argumento não anulável pode ser nulo quando o método retorna o valor especificado bool .
NotNullWhen Pós-condição condicional Um argumento anulável não será nulo quando o método retornar o valor especificado bool .
NotNullIfNotNull Pós-condição condicional Um valor de retorno, propriedade ou argumento não será nulo se o argumento para o parâmetro especificado não for nulo.
MembroNotNull Método e métodos auxiliares de propriedade O membro listado não será nulo quando o método retornar.
MemberNotNullWhen Método e métodos auxiliares de propriedade O membro listado não será nulo quando o método retornar o valor especificado bool .
DoesNotReturn Código inacessível Um método ou propriedade nunca retorna. Por outras palavras, lança sempre uma exceção.
DoesNotReturnIf Código inacessível Esse método ou propriedade nunca retorna se o parâmetro associado bool tiver o valor especificado.

As descrições anteriores são uma referência rápida ao que cada atributo faz. As seções a seguir descrevem o comportamento e o significado desses atributos mais detalhadamente.

Pré-condições: AllowNull e DisallowNull

Considere uma propriedade de leitura/gravação que nunca retorna null porque tem um valor padrão razoável. Os chamadores passam null para o acessador definido ao defini-lo para esse valor padrão. Por exemplo, considere um sistema de mensagens que pede um nome de tela em uma sala de chat. Se nenhum for fornecido, o sistema gera um nome aleatório:

public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

Quando você compila o código anterior em um contexto nulo esquecido, tudo está bem. Depois de habilitar tipos de referência anuláveis, a ScreenName propriedade se torna uma referência não anulável. Isso é correto para o get acessador: ele nunca retorna null. Os chamadores não precisam verificar a propriedade retornada para null. Mas agora definir a propriedade para null gera um aviso. Para dar suporte a esse tipo de código, adicione o System.Diagnostics.CodeAnalysis.AllowNullAttribute atributo à propriedade, conforme mostrado no código a seguir:

[AllowNull]
public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

Talvez seja necessário adicionar uma using diretiva para System.Diagnostics.CodeAnalysis usar este e outros atributos discutidos neste artigo. O atributo é aplicado à propriedade, não ao set acessador. O AllowNull atributo especifica pré-condições e só se aplica a argumentos. O get acessador tem um valor de retorno, mas sem parâmetros. Portanto, o AllowNull atributo só se aplica ao set acessador.

O exemplo anterior demonstra o que procurar ao adicionar o AllowNull atributo em um argumento:

  1. O contrato geral para essa variável é que ela não deveria ser null, então você quer um tipo de referência não anulável.
  2. Há cenários para um chamador passar null como argumento, embora não sejam o uso mais comum.

Na maioria das vezes, você precisará desse atributo para propriedades, ou in, oute ref argumentos. O AllowNull atributo é a melhor escolha quando uma variável normalmente não é nula, mas você precisa permitir null como uma pré-condição.

Compare isso com cenários para usar DisallowNull: Você usa esse atributo para especificar que um argumento de um tipo de referência anulável não deve ser null. Considere uma propriedade onde null é o valor padrão, mas os clientes só podem defini-la como um valor não nulo. Considere o seguinte código:

public string ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

O código anterior é a melhor maneira de expressar seu design que poderia ReviewComment ser null, mas não pode ser definido como null. Uma vez que esse código é anulável, você pode expressar esse conceito mais claramente para chamadores usando o System.Diagnostics.CodeAnalysis.DisallowNullAttribute:

[DisallowNull]
public string? ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

Em um contexto anulável, o ReviewComment get acessador poderia retornar o valor padrão de null. O compilador avisa que ele deve ser verificado antes do acesso. Além disso, avisa os chamadores que, mesmo que possa ser null, os chamadores não devem defini-lo explicitamente como null. O DisallowNull atributo também especifica uma pré-condição, não afeta o get acessador. Você usa o DisallowNull atributo quando observa essas características sobre:

  1. A variável pode estar null em cenários centrais, muitas vezes quando instanciada pela primeira vez.
  2. A variável não deve ser explicitamente definida como null.

Essas situações são comuns em códigos que originalmente eram nulos. Pode ser que as propriedades do objeto sejam definidas em duas operações de inicialização distintas. Pode ser que algumas propriedades sejam definidas somente após a conclusão de algum trabalho assíncrono.

Os AllowNull atributos e DisallowNull permitem especificar que as pré-condições em variáveis podem não corresponder às anotações anuláveis nessas variáveis. Eles fornecem mais detalhes sobre as características da sua API. Essas informações adicionais ajudam os chamadores a usar sua API corretamente. Lembre-se de especificar pré-condições usando os seguintes atributos:

  • AllowNull: Um argumento não anulável pode ser null.
  • DisallowNull: Um argumento nullable nunca deve ser null.

Pós-condições: MaybeNull e NotNull

Suponha que você tenha um método com a seguinte assinatura:

public Customer FindCustomer(string lastName, string firstName)

Você provavelmente escreveu um método como este para retornar null quando o nome procurado não foi encontrado. O null indica claramente que o registro não foi encontrado. Neste exemplo, você provavelmente alteraria o tipo de retorno de Customer para Customer?. Declarar o valor de retorno como um tipo de referência anulável especifica claramente a intenção dessa API:

public Customer? FindCustomer(string lastName, string firstName)

Por motivos cobertos em Genéricos, a anulabilidade dessa técnica pode não produzir a análise estática que corresponde à sua API. Você pode ter um método genérico que segue um padrão semelhante:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

O método retorna null quando o item procurado não é encontrado. Você pode esclarecer que o método retorna null quando um item não é encontrado adicionando a MaybeNull anotação ao retorno do método:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

O código anterior informa os chamadores que o valor de retorno pode realmente ser nulo. Ele também informa ao compilador que o método pode retornar uma null expressão mesmo que o tipo não seja anulável. Quando você tem um método genérico que retorna uma instância de seu parâmetro type, T, você pode expressar que ele nunca retorna null usando o NotNull atributo.

Você também pode especificar que um valor de retorno ou um argumento não é nulo, mesmo que o tipo seja um tipo de referência anulável. O método a seguir é um método auxiliar que lança se seu primeiro argumento for null:

public static void ThrowWhenNull(object value, string valueExpression = "")
{
    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

Você pode chamar essa rotina da seguinte forma:

public static void LogMessage(string? message)
{
    ThrowWhenNull(message, $"{nameof(message)} must not be null");

    Console.WriteLine(message.Length);
}

Depois de habilitar tipos de referência nulos, você deseja garantir que o código anterior seja compilado sem avisos. Quando o método retorna, o value parâmetro é garantido para não ser nulo. No entanto, é aceitável chamar ThrowWhenNull com uma referência nula. Você pode criar value um tipo de referência anulável e adicionar a NotNull pós-condição à declaração de parâmetro:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
    // other logic elided

O código anterior expressa o contrato existente claramente: os chamadores podem passar uma variável com o null valor, mas o argumento é garantido para nunca ser nulo se o método retornar sem lançar uma exceção.

Você especifica pós-condições incondicionais usando os seguintes atributos:

  • MaybeNull: Um valor de retorno não anulável pode ser null.
  • NotNull: Um valor de retorno anulável nunca será nulo.

Pós-condições condicionais: NotNullWhen, MaybeNullWhene NotNullIfNotNull

Você provavelmente está familiarizado com o string método String.IsNullOrEmpty(String). Esse método retorna true quando o argumento é nulo ou uma cadeia de caracteres vazia. É uma forma de null-check: os chamadores não precisam anular o argumento se o método retornar false. Para tornar um método como esse anulável, defina o argumento como um tipo de referência anulável e adicione o NotNullWhen atributo:

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

Isso informa ao compilador que qualquer código onde o valor de retorno está false não precisa de verificações nulas. A adição do atributo informa a análise estática do compilador que IsNullOrEmpty executa a verificação nula necessária: quando ele retorna false, o argumento não nullé .

string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
    int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

O String.IsNullOrEmpty(String) método será anotado como mostrado acima para o .NET Core 3.0. Você pode ter métodos semelhantes em sua base de código que verificam o estado dos objetos para valores nulos. O compilador não reconhecerá métodos de verificação nula personalizados e você mesmo precisará adicionar as anotações. Quando você adiciona o atributo, a análise estática do compilador sabe quando a variável testada foi anulada verificada.

Outro uso para esses atributos é o Try* padrão. As pós-condições ref e out os argumentos são comunicados através do valor de retorno. Considere este método mostrado anteriormente (em um contexto desabilitado anulável):

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

O método anterior segue uma linguagem típica do .NET: o valor de retorno indica se message foi definido como o valor encontrado ou, se nenhuma mensagem for encontrada, para o valor padrão. Se o método retornar true, o valor de message não é null, caso contrário, o método será definido message como null.

Em um contexto habilitado anulável, você pode comunicar esse idioma usando o NotNullWhen atributo. Ao anotar parâmetros para tipos de referência anuláveis, crie message um string? e adicione um atributo:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message is not null;
}

No exemplo anterior, o valor de message é conhecido por não ser nulo quando TryGetMessage retorna true. Você deve anotar métodos semelhantes em sua base de código da mesma maneira: os argumentos podem ser iguais nulla , e são conhecidos por não serem nulos quando o método retorna true.

Há um atributo final que você também pode precisar. Às vezes, o estado nulo de um valor de retorno depende do estado nulo de um ou mais argumentos. Esses métodos retornarão um valor não nulo sempre que determinados argumentos não nullforem . Para anotar corretamente esses métodos, use o NotNullIfNotNull atributo . Considere o seguinte método:

string GetTopLevelDomainFromFullUrl(string url)

Se o url argumento não for nulo, a saída não nullserá . Depois que as referências anuláveis estiverem habilitadas, você precisará adicionar mais anotações se sua API puder aceitar um argumento nulo. Você pode anotar o tipo de retorno conforme mostrado no código a seguir:

string? GetTopLevelDomainFromFullUrl(string? url)

Isso também funciona, mas muitas vezes forçará os chamadores a implementar verificações extras null . O contrato é que o valor de retorno seria null apenas quando o argumento url é null. Para expressar esse contrato, você deve anotar este método conforme mostrado no código a seguir:

[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)

O exemplo anterior usa o nameof operador para o parâmetro url. Esse recurso está disponível em C# 11. Antes do C# 11, você precisará digitar o nome do parâmetro como uma cadeia de caracteres. O valor de retorno e o argumento foram anotados com a indicação de ? que ambos poderiam ser null. O atributo esclarece ainda que o valor de retorno não será nulo quando o url argumento não nullfor .

Você especifica pós-condições condicionais usando estes atributos:

  • MaybeNullWhen: Um argumento não anulável pode ser nulo quando o método retorna o valor especificado bool .
  • NotNullWhen: Um argumento anulável não será nulo quando o método retornar o valor especificado bool .
  • NotNullIfNotNull: Um valor de retorno não será nulo se o argumento para o parâmetro especificado não for nulo.

Métodos auxiliares: MemberNotNull e MemberNotNullWhen

Esses atributos especificam sua intenção quando você refatorou o código comum de construtores em métodos auxiliares. O compilador C# analisa construtores e inicializadores de campo para certificar-se de que todos os campos de referência não anuláveis foram inicializados antes que cada construtor retorne. No entanto, o compilador C# não controla as atribuições de campo através de todos os métodos auxiliares. O compilador emite aviso CS8618 quando os campos não são inicializados diretamente no construtor, mas sim em um método auxiliar. Você adiciona o MemberNotNullAttribute a uma declaração de método e especifica os campos que são inicializados para um valor não-nulo no método. Por exemplo, considere o seguinte exemplo:

public class Container
{
    private string _uniqueIdentifier; // must be initialized.
    private string? _optionalMessage;

    public Container()
    {
        Helper();
    }

    public Container(string message)
    {
        Helper();
        _optionalMessage = message;
    }

    [MemberNotNull(nameof(_uniqueIdentifier))]
    private void Helper()
    {
        _uniqueIdentifier = DateTime.Now.Ticks.ToString();
    }
}

Você pode especificar vários nomes de campo como argumentos para o construtor de MemberNotNull atributo.

O MemberNotNullWhenAttribute tem um bool argumento. Você usa MemberNotNullWhen em situações em que seu método auxiliar retorna uma bool indicação se seu método auxiliar inicializou campos.

Parar a análise anulável quando o método chamado lança

Alguns métodos, normalmente auxiliares de exceção ou outros métodos de utilidade, sempre saem lançando uma exceção. Ou, um auxiliar pode lançar uma exceção com base no valor de um argumento booleano.

No primeiro caso, você pode adicionar o DoesNotReturnAttribute atributo à declaração de método. A análise de estado nulo do compilador não verifica nenhum código em um método que segue uma chamada para um método anotado com DoesNotReturn. Considere este método:

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

O compilador não emite nenhum aviso após a chamada para FailFast.

No segundo caso, você adiciona o System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute atributo a um parâmetro booleano do método. Você pode modificar o exemplo anterior da seguinte maneira:

private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
    if (isNull)
    {
        throw new InvalidOperationException();
    }
}

public void SetFieldState(object? containedField)
{
    FailFastIf(containedField == null);
    // No warning: containedField can't be null here:
    _field = containedField;
}

Quando o valor do argumento corresponde ao DoesNotReturnIf valor do construtor, o compilador não executa nenhuma análise de estado nulo após esse método.

Resumo

Adicionar tipos de referência anuláveis fornece um vocabulário inicial para descrever suas expectativas de APIs para variáveis que podem ser null. Os atributos fornecem um vocabulário mais rico para descrever o estado nulo das variáveis como pré-condições e pós-condições. Esses atributos descrevem mais claramente suas expectativas e fornecem uma experiência melhor para os desenvolvedores que usam suas APIs.

À medida que você atualiza bibliotecas para um contexto anulável, adicione esses atributos para orientar os usuários de suas APIs para o uso correto. Esses atributos ajudam a descrever completamente o estado nulo de argumentos e valores de retorno.

  • AllowNull: Um campo, parâmetro ou propriedade não anulável pode ser null.
  • DisallowNull: Um campo, parâmetro ou propriedade anulável nunca deve ser null.
  • MaybeNull: Um campo, parâmetro, propriedade ou valor de retorno não anulável pode ser null.
  • NotNull: Um campo, parâmetro, propriedade ou valor de retorno anulável nunca será nulo.
  • MaybeNullWhen: Um argumento não anulável pode ser nulo quando o método retorna o valor especificado bool .
  • NotNullWhen: Um argumento anulável não será nulo quando o método retornar o valor especificado bool .
  • NotNullIfNotNull: Um parâmetro, propriedade ou valor de retorno não será nulo se o argumento para o parâmetro especificado não for nulo.
  • DoesNotReturn: Um método ou propriedade nunca retorna. Por outras palavras, lança sempre uma exceção.
  • DoesNotReturnIf: Este método ou propriedade nunca retorna se o parâmetro associado bool tiver o valor especificado.