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

Em um contexto habilitado para 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 desreferenciar um valor nulo, gerando System.NullReferenceException. Esses atributos fornecem ao compilador informações semânticas sobre o null-state de argumentos, valores retornados e membros do objeto com base no estado dos argumentos e valores retornados. 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.

Vamos começar com um exemplo. Imagine que sua biblioteca tem a API a seguir para recuperar uma cadeia de caracteres de recurso. Este método foi originalmente compilado em um contexto alheio anulável:

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 null-state desses parâmetros:

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

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

Observação

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

Atributo Categoria Significado
AllowNull Pré-condição Um parâmetro, um campo ou uma propriedade não anulável pode ser nulo.
DisallowNull Pré-condição Um parâmetro, campo ou propriedade anulável nunca deve ser nulo.
MaybeNull 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 anulável, um campo, uma propriedade ou um valor retornado 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 bool especificado.
NotNullWhen Pós-condição condicional Um argumento anulável não pode ser nulo quando o método retorna o valor bool especificado.
NotNullIfNotNull Pós-condição condicional Um valor retornado, uma propriedade ou um argumento não será nulo se o argumento para o parâmetro especificado não for nulo.
MemberNotNull Métodos auxiliares de método e Propriedade O membro listado não será nulo quando o método retornar.
MemberNotNullWhen Métodos auxiliares de método e Propriedade O membro listado não será nulo quando o método retornar o valor bool especificado.
DoesNotReturn Código inacessível Um método ou propriedade nunca retorna. Em outras palavras, ele sempre gera uma exceção.
DoesNotReturnIf Código inacessível Esse método ou propriedade nunca retornará 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 mais detalhadamente o comportamento e o significado desses atributos.

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 solicita um nome de tela em uma sala de chat. Se nenhum for fornecido, o sistema gerará 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 alheio anulável, tudo está bem. Depois de habilitar tipos de referência anuláveis, a propriedade ScreenName se tornará uma referência não anulável. Isso está correto para o acessador get: ele nunca retorna null. Os chamadores não precisam verificar a propriedade retornada para null. Mas agora definir a propriedade como null gera um aviso. Para dar suporte a esse tipo de código, adicione o atributo System.Diagnostics.CodeAnalysis.AllowNullAttribute à propriedade, conforme mostrado no seguinte código:

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

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

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

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

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

Contraste isso com cenários para uso 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 em que null é o valor padrão, mas os clientes só podem defini-lo 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 ReviewComment pode ser null, mas não pode ser definido como null. Depois que esse código for anulável, você poderá expressar esse conceito com mais clareza aos 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 ReviewCommentget acessador poderia retornar o valor padrão de null. O compilador avisa que ele deve ser verificado antes do acesso. Além disso, ele avisa os chamadores que, embora possa ser null, os chamadores não devem defini-lo explicitamente como null. O atributo DisallowNull também especifica uma pré-condição, não afeta o acessador get. Você usa o atributo DisallowNull quando observa essas características sobre:

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

Essas situações são comuns em código originalmente nulo alheio. 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 atributos AllowNull e DisallowNull permitem que você especifique 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 de 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 nulo.
  • DisallowNull: um argumento anulável nunca deve ser nulo.

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

Considere 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. 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 retornado como um tipo de referência anulável especifica claramente a intenção dessa API:

public Customer? FindCustomer(string lastName, string firstName)

Por motivos abordados em Anulabilidade de genéricos, essa 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 anotação MaybeNull ao retorno do método:

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

O código anterior informa aos chamadores que o valor retornado pode realmente ser nulo. Ele também informa ao compilador que o método pode retornar uma expressão null 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 de tipo, Tvocê pode expressar que ele nunca retorna null usando o atributo NotNull.

Você também pode especificar que um valor retornado ou um argumento não seja nulo, mesmo que o tipo seja um tipo de referência anulável. O método a seguir é um método auxiliar que gera 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 maneira:

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 parâmetro value tem a garantia de não ser nulo. No entanto, é aceitável chamar ThrowWhenNull com uma referência nula. Você pode fazer value um tipo de referência anulável e adicionar a pós-condição NotNull à 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 claramente o contrato existente: os chamadores podem passar uma variável com o valor null, mas é garantido que o argumento nunca será nulo se o método retornar sem gerar uma exceção.

Especifique pós-condições incondicional usando os seguintes atributos:

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

Pós-condições condicionais: NotNullWhen, MaybeNullWhen e 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 verificação nula: os chamadores não precisam verificar nulo o argumento se o método retornar false. Para tornar um método como esse anulável consciente, você definiria o argumento como um tipo de referência anulável e adicionaria o atributo NotNullWhen:

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

Isso informa ao compilador que qualquer código em que o valor retornado é 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 método String.IsNullOrEmpty(String) será anotado conforme 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 em busca de valores nulos. O compilador não reconhecerá métodos personalizados de verificação nula e você precisará adicionar as anotações por conta própria. Quando você adiciona o atributo, a análise estática do compilador sabe quando a variável testada foi verificada nulo.

Outro uso para esses atributos é o padrão Try*. As pós-condições para argumentos ref e out são comunicados por meio do valor retornado. Considere esse método mostrado anteriormente (em um contexto nulo desabilitado):

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 um idioma .NET típico: o valor retornado 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 não message será nulo; caso contrário, o método definirá message como nulo.

Em um contexto habilitado para anulável, você pode comunicar esse idioma usando o atributo NotNullWhen. Quando você anotar parâmetros para tipos de referência anuláveis, faça de 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 null 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 retornado 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 forem null. Para anotar corretamente esses métodos, use o NotNullIfNotNull atributo. Considere o método a seguir:

string GetTopLevelDomainFromFullUrl(string url)

Se o argumento url não for nulo, a saída não será null. Depois que as referências anuláveis forem 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 null extras. O contrato é que o valor retornado seria null somente quando o argumento url for null. Para expressar esse contrato, você anotaria esse método, conforme mostrado no código a seguir:

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

O exemplo anterior usa o operador nameof para o parâmetro url. Este recurso está disponível no C# 11. Antes do C# 11, você precisará digitar o nome do parâmetro como uma cadeia de caracteres. O valor retornado e o argumento foram anotados com a indicação ? de que qualquer um poderia ser null. O atributo esclarece ainda mais que o valor retornado não será nulo quando o argumento url não for null.

Especifique 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 bool especificado.
  • NotNullWhen: Um argumento anulável não será nulo quando o método retornar o valor bool especificado.
  • NotNullIfNotNull: um valor de retorno não é 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ê refatora o código comum de construtores em métodos auxiliares. O compilador C# analisa construtores e inicializadores de campo para garantir que todos os campos de referência não anuláveis tenham sido inicializados antes de cada construtor retornar. No entanto, o compilador C# não acompanha as atribuições de campo em todos os métodos auxiliares. O compilador emite um aviso CS8618 quando os campos não são inicializados diretamente no construtor, mas sim em um método auxiliar. Adicione MemberNotNullAttribute a uma declaração de método e especifique os campos inicializados para um valor não nulo no método. Por exemplo, considere o exemplo a seguir:

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 atributo MemberNotNull.

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

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

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

No primeiro caso, você pode adicionar o atributo DoesNotReturnAttribute à declaração do método. A análise de null-state 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 avisos após a chamada para FailFast.

No segundo caso, você adiciona o System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute atributo a um parâmetro booliano 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 valor do construtor DoesNotReturnIf, o compilador não executa nenhuma análise null-state 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 avançado para descrever o estado nulo de 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 ao uso correto. Esses atributos ajudam você a descrever totalmente o null-state dos argumentos e os valores retornados.

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