Expressando intenção

Concluído

Na unidade anterior, você aprendeu como o compilador C# pode executar a análise estática para ajudar a protegê-lo de NullReferenceException. Você também aprendeu como habilitar um contexto anulável. Nesta unidade, você aprenderá mais sobre como expressar explicitamente sua intenção dentro de um contexto anulável.

Como declarar variáveis

Com um contexto anulável habilitado, você tem mais visibilidade sobre como o compilador vê seu código. Você pode executar ações com base nos avisos gerados de um contexto habilitado para valor nulo e, fazendo isso, você está definindo explicitamente suas intenções. Por exemplo, vamos continuar examinando o código FooBar e analisar a declaração e a atribuição:

// Define as nullable
FooBar? fooBar = null;

Observe o ? adicionado a FooBar. Isso informa ao compilador que você pretende explicitamente que fooBar seja anulável. Se você não pretende que fooBar permita um valor nulo, mas ainda deseja evitar o aviso, considere o seguinte:

// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;

Este exemplo adiciona o operador tolerante a nulos (!) a null, que instrui o compilador de que você está inicializando explicitamente essa variável como nula. O compilador não emitirá avisos sobre essa referência ser nula.

Uma boa prática é atribuir suas variáveis não anuláveis valores que não sejam null quando elas são declaradas, se possível:

// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");

Operadores

Conforme discutido na unidade anterior, o C# define vários operadores para expressar sua intenção em relação aos tipos de referência anuláveis.

Operador tolerante a nulos (!)

Você foi apresentado ao operador tolerante a nulos (!) na seção anterior. Ele informa ao compilador para ignorar o aviso CS8600. Essa é uma maneira de informar ao compilador que você sabe o que está fazendo, mas vem com a advertência de que você deve realmente saber o que está fazendo!

Quando você inicializar tipos não anuláveis enquanto um contexto anulável estiver habilitado, talvez seja necessário pedir explicitamente ao compilador para perdoar. Por exemplo, considere o seguinte código:

#nullable enable

using System.Collections.Generic;

var fooList = new List<FooBar>
{
    new(Id: 1, Name: "Foo"),
    new(Id: 2, Name: "Bar")
};

FooBar fooBar = fooList.Find(f => f.Name == "Bar");

// The FooBar type definition for example.
record FooBar(int Id, string Name);

No exemplo anterior, FooBar fooBar = fooList.Find(f => f.Name == "Bar"); gera um aviso CS8600 porque Find poderá retornar null. nullIsso pode ser atribuído a fooBar, que não permite valor anulável neste contexto. No entanto, neste exemplo artificial, sabemos que Find nunca será retornado null como escrito. Você pode expressar essa intenção para o compilador com o operador tolerante a nulos:

FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;

Observe o ! no final de fooList.Find(f => f.Name == "Bar"). Isso informa ao compilador que você sabe que o objeto retornado pelo método Find pode ser null e não tem problema.

Você também pode aplicar o operador tolerante a valores nulos a um objeto embutido antes de uma chamada de método ou de uma avaliação de propriedade. Considere outro exemplo artificial:

List<FooBar>? fooList = FooListFactory.GetFooList();

// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning

static class FooListFactory
{
    public static List<FooBar>? GetFooList() =>
        new List<FooBar>
        {
            new(Id: 1, Name: "Foo"),
            new(Id: 2, Name: "Bar")
        };
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

No exemplo anterior:

  • GetFooList é um método estático que retorna um tipo anulável, List<FooBar>?.
  • fooList é atribuído ao valor retornado por GetFooList.
  • O compilador gera um aviso sobre fooList.Find(f => f.Name == "Bar"); porque o valor atribuído a fooList pode ser null.
  • Supondo que fooList não seja null, Find pode retornar null, mas sabemos que não será, portanto, o operador tolerante a nulos será aplicado.

Você pode aplicar o operador tolerante a nulos a fooList para desabilitar o aviso:

FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;

Observação

Você deve usar o operador que tolerante a valores nulos com cautela. Usá-lo simplesmente para descartar um aviso significa que você está dizendo ao compilador para não ajudá-lo a descobrir possíveis incidentes nulos. Use-o com moderação e apenas quando tiver certeza disso.

Para obter mais informações, veja Operador ! (tolerante a valores nulos) (referência de C#).

Operador de avaliação de nulo (??)

Ao trabalhar com tipos anuláveis, talvez seja necessário avaliar se eles estão null no momento e tomar certas ações. Por exemplo, quando um tipo anulável tiver sido atribuído null ou não tiver sido inicializado, talvez seja necessário atribuí-los um valor não nulo. É aí que o operador de avaliação de nulo (??) é útil.

Considere o exemplo a seguir:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    salesTax ??= DefaultStateSalesTax.Value;

    // Safely use salesTax object.
}

No código anterior do C#:

  • O parâmetro salesTax é definido como sendo IStateSalesTax anulável.
  • No corpo do método, o salesTax é condicionalmente atribuído usando o operador de avaliação de nulo.
    • Isso garante que, se salesTax tiver sido passado como null, ele terá um valor.

Dica

Isso é funcionalmente equivalente ao seguinte código C#:

public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
    if (salesTax is null)
    {
        salesTax = DefaultStateSalesTax.Value;
    }

    // Safely use salesTax object.
}

Veja um exemplo de outra expressão C# comum em que o operador de avaliação de nulo pode ser útil:

public sealed class Wrapper<T> where T : new()
{
    private T _source;

    // If given a source, wrap it. Otherwise, wrap a new source:
    public Wrapper(T source = null) => _source = source ?? new T();
}

O código anterior do C#:

  • Define uma classe wrapper genérica, em que o parâmetro de tipo genérico é restrito a new().
  • O construtor aceita um parâmetro T source que usa como padrão null.
  • O _source encapsulado é condicionalmente inicializado para um new T().

Para obter mais informações, confira Operadores ?? e ??= (referência de C#).

Operador condicional nulo (?.)

Ao trabalhar com tipos anuláveis, talvez seja necessário executar ações condicionalmente com base no estado de um objeto null. Por exemplo: na unidade anterior, o registro FooBar foi usado para demonstrar NullReferenceException pela desreferência de null. Isso foi causado quando ToString foi chamado. Considere este mesmo exemplo, mas agora aplicando o operador condicional nulo:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

O código anterior do C#:

  • Desreferenciar fooBar condicionalmente, atribuindo o resultado de ToString à variável str.
    • A variável str é do tipo string? (cadeia de caracteres anulável).
  • Ele grava o valor de str na saída padrão, que é nada.
  • A chamada a Console.Write(null) é válida, ou seja, não há nenhum aviso.
  • Você receberia um aviso se fosse chamar Console.Write(str.Length) porque estaria potencialmente desreferenciando o nulo.

Dica

Isso é funcionalmente equivalente ao seguinte código C#:

using System;

// Declare variable and assign it as null.
FooBar fooBar = null;

// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);

// The FooBar type definition.
record FooBar(int Id, string Name);

Você pode combinar o operador para expressar ainda mais sua intenção. Por exemplo, você poderia encadear os operadores ?. e ??:

FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown

Para obter mais informações, veja Operadores ?. e ?[] (condicional nulo).

Resumo

Nesta unidade, você aprendeu a expressar a sua intenção de nulidade no código. Na próxima unidade, você aplicará o que aprendeu a um projeto existente.

Verificar seu conhecimento

1.

Qual é o valor default do tipo de referência string?

2.

Qual é o comportamento esperado da desreferenciação de null?

3.

O que acontece quando este código C# throw null; é executado?

4.

Qual instrução é mais precisa em relação aos tipos de referência anuláveis?