Featured image of post Segurança nula em tempo de compilação: como evitar NullReferenceException em C#

Segurança nula em tempo de compilação: como evitar NullReferenceException em C#

Capturando erros nulos em tempo de compilação

Introdução Link to this section

Softwares podem falhar em dois momentos diferentes: tempo de compilação e tempo de execução. No contexto de erros, nulo é um problema porque os efeitos de não tratá-los corretamente são percebidos apenas em tempo de execução.

Neste post, mostrarei como usar um recurso da linguagem C# para mover os erros nulos para o tempo de compilação e nos ajudar a evitá-los em tempo de execução.

O problema com nulo Link to this section

Nulo é um valor que não é um valor. Ele é usado por tipos de referência para indicar que a variável não está apontando para nenhum valor.

Isso cria um caso especial que precisa ser verificado toda vez que a variável é acessada ou lançará uma exceção em tempo de execução quando o valor for nulo.

Este código, por exemplo, será compilado normalmente:

1
2
3
string nullHere = null;

Console.WriteLine($"Length: {nullHere.Length}"); //Tentando acessar o Length de nulo

Mas lançará uma NullReferenceException em tempo de execução:

Exceção não tratada. System.NullReferenceException:
Referência de objeto não definida para uma instância de um objeto.

A solução é verificar se é null antes de acessar qualquer propriedade ou método de tipo de referência:

1
2
3
4
5
6
string nullHere = null;

if (nullHere != null)
{
    Console.WriteLine($"Length: {nullHere.Length}");
}

O problema é lembrar de verificar se é nulo toda vez que um objeto pode ser nulo. É aí que os Tipos de referência anuláveis vêm para o resgate.

Tipos de referência anuláveis Link to this section

Tipos de referência, como o nome sugere, armazenam referências (ponteiros) para seus dados.

C# fornece os seguintes tipos de referência:

  • string
  • object
  • class
  • record
  • interface
  • delegate
  • dynamic

Todos esses tipos podem ter null atribuído a eles, tornando-os um risco para uma NullReferenceException.

C# 8 introduziu os Tipos de Referência Anuláveis. É uma maneira de dizer que as variáveis podem conter nulo e receber avisos toda vez que seus membros são acessados sem verificar se há nulos.

Para declarar tipos de referência anuláveis, usamos ? após o nome do tipo:

string? stringThatMayBeNull = null; //String anulável

Person? personThatMayBeNull = null; //Pessoa anulável

List<Person>? personThatMayBeNull = null; //Lista anulável de Pessoa não anulável

List<Person?> personThatMayBeNull = new(){ null }; //Lista não anulável de Pessoa anulável

Para habilitar os tipos de referência anuláveis, basta adicionar a seguinte propriedade aos seus projetos:

<Nullable>enable</Nullable>

Avisos de anulabilidade Link to this section

Quando habilitamos os tipos de referência anuláveis em nossos projetos, começamos a receber avisos ao atribuir null a tipos não anuláveis:

Também começamos a receber avisos ao acessar um membro de tipo anulável quando ele pode ser nulo:

O compilador sabe quando uma variável não é nula.

Por exemplo, quando verificamos se é nulo em uma instrução if:

E quando atribuímos um valor não anulável a uma variável anulável:

Tratando nulos Link to this section

Além de verificar se há nulos com instruções if, podemos usar operadores nulos que o C# fornece para tornar o código mais limpo.

Operador condicional nulo Link to this section

O operador condicional nulo (?.) nos permite acessar um membro de tipo de referência somente se seu valor não for nulo.

1
2
3
string? nullHere = null;

Console.WriteLine($"Length: {nullHere?.Length}"); //Nenhuma NullReferenceException aqui

Operador de perdão nulo Link to this section

O operador de perdão nulo (!.) informa ao compilador que temos certeza de que uma variável de tipo anulável que pode ser nula não é nula.

Isso é comumente útil ao escrever testes automatizados porque você conhece os resultados que serão retornados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface INullSampleUseCase
{
    Person? GetPossibleNullPerson();
}

public class MyClass
{
    private readonly INullSampleUseCase _nullSampleUseCase;

    public MyClass(INullSampleUseCase nullSampleUseCase)
    {
        _nullSampleUseCase = nullSampleUseCase;
    }

    public Person? GetThePerson()
    {
        return _nullSampleUseCase.GetPossibleNullPerson();
    }
}

[Fact]
public void GetThePerson_ShouldGetRightPerson()
{
    var nullSampleUseCaseMock = new Mock<INullSampleUseCase>();

    nullSampleUseCaseMock
        .Setup(m => m.GetPossibleNullPerson())
        .Returns(randomPerson);

    var myClass = new MyClass(nullSampleUseCaseMock.Object);

    var personReturned = myClass.GetThePerson();

    Assert.Equal(randomPerson.FirstName, personReturned!.FirstName); //Tenho certeza de que personReturned não será nulo aqui!
}

Operador de coalescência nula Link to this section

O operador de coalescência (??) é usado para atribuir o valor do operando direito se o valor do operando esquerdo for nulo. É usado para atribuir um tipo de referência anulável a um tipo de referência não anulável, voltando a um valor padrão em caso de nulo.

1
2
3
4
5
string? maybeNullHere = getSomeValueOrNull();

string notNullHere = maybeNullHere ?? "Valor padrão"; //Use "Valor padrão" se maybeNullHere for nulo

Console.WriteLine($"Length: {notNullHere.Length}");

Impondo verificações nulas em tempo de compilação Link to this section

Para impor verificações nulas em todos os lugares, podemos aumentar a severidade dos avisos para Error. Desta forma, não será possível compilar os projetos sem verificar adequadamente se há nulos.

Infelizmente, as mensagens do compilador para anuláveis não estão nas categorias de qualidade de código e estilo de código explicadas em meu post anterior e não podem ser definidas como erros, a menos que sejam feitas por código de mensagem:

1
2
3
4
dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8670.severity = error
dotnet_diagnostic.CS8601.severity = error
...

Para fazer isso, defina a propriedade TreatWarningsAsErrors como Nullable em todos os projetos:

<TreatWarningsAsErrors>Nullable</TreatWarningsAsErrors>

Depois disso, todos os avisos de mensagens de anulabilidade terão sua severidade igual a Error:

💡 Podemos definir TreatWarningsAsErrors como true para tratar todos os avisos como erros.

Propriedades obrigatórias Link to this section

Com os tipos anuláveis habilitados, receberemos os seguintes erros em classes/registros que têm tipos de referência não anuláveis não inicializados no construtor ou com valores padrão:

C# 11 introduziu o modificador required. Ele nos permite ter tipos de referência não anuláveis sem inicializá-los no construtor, forçando-nos a especificar os valores para essas propriedades ao instanciar um objeto.

Primeiro, adicionamos o modificador required às propriedades:

Então, ao instanciar o objeto, precisamos passar os valores para as propriedades obrigatórias:

Se não especificarmos os valores, o compilador nos avisará:

Testes automatizados Link to this section

Vale a pena notar que o uso do operador condicional nulo e do operador de coalescência nula influenciará a cobertura de código do projeto. Eles serão tratados como branches e terão que ter testes para ambos os cenários.

Por exemplo, se tivermos esta classe boba:

1
2
3
4
5
6
7
public class NullSampleUseCase
{
    public string GetFormatedStringLength(string? maybeNullHere)
    {
        return $"Length: {maybeNullHere?.Length}";
    }
}

E este teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class NullSampleUseCaseTests
{
    [Fact]
    public void WhenNotNull_ShouldPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength("My string");

        Assert.Equal($"Length: 9", formatedLength);
    }
}

A cobertura de código será de apenas 50% porque o teste não está cobrindo o cenário de recebimento de nulo.

Criando um novo teste para o cenário nulo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class NullSampleUseCaseTests
{
    [Fact]
    public void WhenNotNull_ShouldPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength("My string");

        Assert.Equal($"Length: 9", formatedLength);
    }

    [Fact]
    public void WhenNull_ShouldNotPrintLength()
    {
        var nullSampleUseCase = new NullSampleUseCase();
        var formatedLength = nullSampleUseCase.GetFormatedStringLength(null);

        Assert.Equal($"Length: ", formatedLength);
    }
}

A cobertura de código será de 100% para esta classe:

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Criado com Hugo
Tema Stack desenvolvido por Jimmy