Resolver avisos anuláveis

Tip

Novo em tipos de referência anuláveis? Leia primeiro os tipos de referência anuláveis para compreender as anotações e a análise de estado nulo. Este artigo assume que está a ver avisos num projeto onde a funcionalidade está ativada.

À procura de um código de erro específico do compilador? O artigo de referência Resolver avisos de anulabilidade lista todos os avisos CS86xx e a técnica correspondente.

Quando ativas os tipos de referência anuláveis, o compilador emite avisos em todo o lado onde o comportamento do teu código não corresponde às suas anotações. A maioria dos avisos segue um pequeno conjunto de padrões. Depois de reconhecer o padrão, a solução é geralmente uma de cinco técnicas:

  • Adiciona uma verificação de valor nulo.
  • Adicione ou remova uma anotação ? ou !.
  • Adicione um atributo que descreva o contrato nulo.
  • Inicializar as variáveis corretamente.
  • Verifica a definição do projeto.

Este artigo apresenta cada técnica com um exemplo representativo. O objetivo não é silenciar os avisos. É para tornar explícita a intenção de gestão nula do código, para que o compilador chegue às mesmas conclusões que tu.

Estado de nulidade: o que o compilador controla

Antes de analisar as técnicas, é útil saber como o compilador acompanha potenciais violações do estado nulo. Enquanto lê o seu código, o compilador acompanha o estado nulo de cada expressão: a sua análise sobre se a expressão pode estar null nesse ponto do código. O estado nulo é um de dois valores:

  • not-null — o compilador pode provar que a expressão não é null aqui. Pode usá-lo em segurança sem necessidade de verificação.
  • Maybe-Null — o compilador não pode excluir null. Usar a expressão sem verificar produz um aviso.

O estado nulo de uma variável muda à medida que o compilador segue o seu código. Um método que pode devolver null gera um resultado potencialmente nulo. Uma if (x is not null) verificação restringe x a não nulo dentro do bloco if. Os avisos que vês são o compilador a dizer-te que determinou que uma expressão está num estado talvez-nulo e que estás prestes a usá-la como se não fosse nula. Cada técnica no resto deste artigo é uma forma diferente de fornecer ao compilador a informação necessária para garantir que uma expressão não é nula antes de a utilizar.

Adicionar uma verificação nula

O aviso mais comum é a possível desreferenciação de um valor nulo. O compilador rastreava o estado nulo de uma variável até talvez-nulo e via a variável usada sem verificação:

public static int LengthOfMessageUnsafe(string? message)
{
    // Warning CS8602: dereference of a possibly null reference.
    return message.Length;
}

A solução costuma ser uma cláusula de guarda. Uma cláusula de guarda é uma verificação no topo de um método ou bloco que retorna ou lança quando uma entrada é inválida. Só o caminho seguro continua. Uma vez executada a verificação, o compilador atualiza o estado nulo da variável para não-nulo no caminho seguro:

public static int DereferenceFixed(string? message)
{
    if (message is null)
    {
        return 0;
    }

    // No warning: the compiler knows message is not-null on this path.
    return message.Length;
}

Correspondência de padrões (expressões como is null ou is { } que testam a forma de um valor), ??, e ??= incluem verificações nulas:

public static int NullOperatorsFix(string? message)
{
    // ?. evaluates to null if message is null; ?? supplies the fallback value.
    int length = message?.Length ?? 0;

    // Pattern matching narrows the type on the matching branch.
    if (message is { Length: > 0 })
    {
        length = message.Length;
    }

    return length;
}

O padrão de propriedade { Length: > 0 } só corresponde quando message não é nulo e a propriedade Length é superior a zero, pelo que o compilador trata message como não nulo dentro do bloco if. Um teste mais is not null simples produz o mesmo estreitamento do estado nulo sem inspecionar quaisquer propriedades.

Para uma visita detalhada aos operadores, veja Operadores nulos.

Ajustar anotações

O compilador também avisa quando o seu código atribui uma expressão maybe-null a uma variável não anulável. Esse aviso significa uma de duas coisas:

  • A variável deve permitir valores nulos. Nesse caso, adiciona um ? ao tipo.
  • A expressão nunca produz um valor nulo. Anota a API que o produziu.
public static void AssignmentWarning()
{
    // Warning CS8600: converting null literal or possible null value to non-nullable type.
    string name = Lookup("nobody");
    Console.WriteLine(name);
}

Se Lookup devolve legitimamente null, altere o site da chamada para aceitar o valor em falta:

public static void AssignmentFixed()
{
    string? name = Lookup("somebody");
    if (name is not null)
    {
        Console.WriteLine(name);
    }
}

Se Lookup nunca devolver nulo, altere a assinatura para devolver um tipo de referência não anulável. Cenários em que o estado nulo do valor devolvido depende da entrada, veja a secção seguinte sobre atributos de análise nula.

Utilize o operador de supressão de null ! apenas quando puder garantir que um valor não é null, mas não conseguir expressar essa garantia no sistema de tipos. Cada ! é um ponto em que o compilador já não te pode proteger, por isso, é preferível adicionar uma verificação ou anotar a API de origem.

Adicionar um atributo de análise nula

Por vezes, a solução certa não está no local da chamada. A assinatura de um método não capta a relação entre as suas entradas e saídas com precisão suficiente, e o compilador emite avisos dentro de código que seria de outra forma seguro:

public static bool IsPresent(string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithoutAttribute(string? text)
{
    if (IsPresent(text))
    {
        // Warning CS8602: dereference of a possibly null reference.
        // The signature doesn't tell the compiler text is not-null here.
        Console.WriteLine(text.Length);
    }
}

O corpo de IsPresent prova que o argumento não é nulo quando o método retorna true, mas a assinatura não o indica. Adicione um atributo de análise anulável para tornar o contrato parte da API:

public static bool AttributedIsPresent([NotNullWhen(true)] string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithAttribute(string? text)
{
    if (AttributedIsPresent(text))
    {
        // No warning: the attribute tells the compiler text is not-null.
        Console.WriteLine(text.Length);
    }
}

Atributos comuns incluem:

A lista completa encontra-se em atributos de análise estática anulável.

Inicializar membros não anuláveis

Um aviso de construtor significa que um campo, propriedade ou auto-propriedade não anulável (uma propriedade que utiliza o campo de suporte gerado pelo compilador, como public string Name { get; set; }) sai do construtor sem lhe ser atribuído um valor não nulo:

public class PersonUninitialized
{
    // Warning CS8618: Non-nullable property 'Name' is uninitialized.
    public string Name { get; set; }
}

Tens várias formas de lidar com isso. Escolhe aquele que melhor se adequa à tua intenção de design.

Requer o valor como argumento construtor. Use um construtor primário (parâmetros declarados no próprio tipo, disponíveis em todo o corpo) ou um construtor regular que inicializa a propriedade:

public class PersonInjected(string name)
{
    public string Name { get; } = name;
}

Defina a propriedade como required. O chamador deve inicializá-lo recorrendo a um inicializador de objetos (a sintaxe { Property = value } que se segue a new):

public class PersonRequired
{
    public required string Name { get; init; }
}

Inicializar com um valor predefinido. Quando o tipo tem um valor vazio significativo, inicialize na declaração:

public class PersonInitialized
{
    public string Name { get; set; } = "John Doe";
}

Tip

Escolha esta técnica apenas quando o tipo tiver um valor predefinido verdadeiramente bom: um valor que seja uma instância válida e totalmente funcional, que o código chamador possa utilizar. Exemplos incluem coleções vazias. Não inventes um valor sentinela (um valor de substituição, como String.Empty, "N/A", "unknown" ou -1, que tratas como "sem valor") em vez de null: isso silencia o aviso, mas todos os chamadores têm de conhecer e verificar esse valor sentinela, e o sistema de tipos não pode ajudar. Quando não existir um bom incumprimento, torne a propriedade anulável em vez disso.

Torna a propriedade anulável. Quando o valor realmente estiver em falta, altere o tipo para nullable:

public class PersonOptional
{
    public string? Name { get; set; }
}

Se um método auxiliar inicializar o membro, anote o método auxiliar com MemberNotNullAttribute para que o compilador possa atribuir-lhe as chamadas.

Verificar a definição do projeto

Novos projetos em C# permitem por defeito tipos de referência anuláveis, por isso a maior parte do código que escreves ou lês já tem a funcionalidade ativada. Geralmente, não precisas de configurar nada. Se tens curiosidade em saber se um projeto tem isso ativado, ou se precisas de mudar a definição, procura o <Nullable> elemento no .csproj:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

Os valores suportados comuns são enable (o padrão para novos projetos) e disable. Se o elemento estiver em falta, o projeto usa o que o SDK e o framework alvo definiram por padrão.

Se precisar de ativar a anulabilidade apenas numa parte de um ficheiro com diretivas #nullable, ou de usar os modos parciais warnings e annotations ao migrar código existente, consulte Estratégias de migração para anulabilidade.

Onde ir a seguir

Quando um aviso não se enquadra em nenhum destes padrões, o artigo de referência sobre avisos anuláveis do Resolve lista a técnica para cada aviso CS86xx emitido pelo compilador.

Para planear uma migração que permita progressivamente tipos de referência anuláveis numa base de código existente, veja Estratégias de migração anuláveis.