Expressar a intenção

Concluído

Na unidade anterior, você aprendeu como o compilador C# pode executar análise estática para ajudar a proteger contra 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.

Declarando variáveis

Com um contexto anulável habilitado, você tem mais visibilidade de como o compilador vê seu código. Você pode agir de acordo com os avisos gerados a partir de um contexto nulo e, ao fazer isso, está definindo explicitamente suas intenções. Por exemplo, vamos continuar examinando o FooBar código e examinar a declaração e a atribuição:

// Define as nullable
FooBar? fooBar = null;

Observe o ? adicionado ao FooBar. Isso informa ao compilador que você pretende explicitamente que fooBar seja anulável. Se você não pretende fooBar ser anulável, mas ainda assim 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 null-forgiving (!) ao null, que instrui o compilador que você está inicializando explicitamente essa variável como null. O compilador não emitirá avisos sobre essa referência ser nula.

Uma boa prática é atribuir suas variáveis não anuláveis semnull valores quando elas forem 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 torno de tipos de referência anuláveis.

Operador de perdão nulo (!)

Você foi apresentado ao operador de tolerância nula (!) na seção anterior. Ele diz ao compilador para ignorar o aviso CS8600. Esta é uma maneira de dizer ao compilador que você sabe o que está fazendo, mas vem com a ressalva de que você deve realmente saber o que está fazendo!

Quando você inicializa tipos não anuláveis enquanto um contexto anulável está habilitado, talvez seja necessário pedir perdão explicitamente ao compilador. 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 pode retornar null. Esta possibilidade null seria atribuída à fooBar, que não é anulável neste contexto. No entanto, neste exemplo inventado, sabemos que Find nunca voltará null como escrito. Você pode expressar essa intenção para o compilador com o operador null-forpering:

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 Find método pode ser null, e tudo bem.

Você também pode aplicar o operador de tolerância nula a um objeto embutido antes de uma chamada de método ou avaliação de propriedade. Considere outro exemplo inventado:

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 o valor retornado por GetFooList.
  • O compilador gera um aviso porque fooList.Find(f => f.Name == "Bar"); o valor atribuído a fooList pode ser null.
  • Supondo fooList que não nullé, Find pode retornar null, mas sabemos que não, então o operador de perdão nulo é aplicado.

Você pode aplicar o operador null-forgiving para fooList desativar o aviso:

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

Nota

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

Para mais informações, consulte ! operador (null-forgiving) (referência C#).

Operador de coalescência nula (??)

Ao trabalhar com tipos anuláveis, talvez seja necessário avaliar se eles estão atualmente null e tomar determinadas medidas. Por exemplo, quando um tipo anulável tiver sido atribuído null ou não tiver sido inicializado, talvez seja necessário atribuir-lhe um valor não nulo. É aí que o operador de coalescência nula (??) é útil.

Considere o seguinte exemplo:

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

    // Safely use salesTax object.
}

No código C# anterior:

  • O salesTax parâmetro é definido como sendo um anulável IStateSalesTax.
  • Dentro do corpo do método, o salesTax é atribuído condicionalmente usando o operador de coalescência nulo.
    • Isso garante que se salesTax foi passado como null que terá um valor.

Gorjeta

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.
}

Aqui está um exemplo de outra linguagem C# comum onde o operador de coalescência nula 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 C# anterior:

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

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

Operador null-conditional (?.)

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

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 C# anterior:

  • Desreferencia fooBarcondicionalmente, atribuindo o resultado de ToString à str variável.
    • A str variável é do tipo string? (cadeia anulável).
  • Ele grava o valor da str saída padrão, que não é nada.
  • A chamada Console.Write(null) é válida, por isso não há avisos.
  • Você receberia um aviso se Console.Write(str.Length) ligasse porque estaria potencialmente desreferenciando null.

Gorjeta

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 operador para expressar ainda mais sua intenção. Por exemplo, você pode encadear os ?. operadores e ?? :

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

Para obter mais informações, consulte os operadores ?. e ?[] (condicional nulo).

Resumo

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

Verifique o seu conhecimento

1.

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

2.

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

3.

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

4.

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