C# -  Técnicas para tratamento de null


  Hoje veremos algumas técnicas para tratar null e assim maximizar a robustez do código.

No C# null é um valor especial que representa a ausência de valor ou um valor padrão para tipos de referência. Ele indica que a variável não faz referência a uma instância de um objeto e pode ser atribuída a variáveis de tipos de referência e variáveis de tipo de valor quando eles forem tipos de valor anuláveis.

O não tratamento adequado do valor null pode resultar em problemas potenciais como NullReferenceException, comportamentos ou resultados não esperados, impacto no desempenho e complexidade do código. Vejamos cada uma destas situações com mais detalhes:

Nota: Os exemplos usados foram criados no .NET 7.0 usando o Top Level Statement

1- NullReferenceException

Esta exceção é lançada quando você tenta acessar um membro de um objeto que é null;

  string minhaString = null;

 Console.WriteLine(minhaString.Length); // Lança um NullReferenceException
 

Neste exemplo, a variável minhaString recebe um valor nulo, ao tentarmos acessar sua propriedade Length sem verificar se ela é nula resulta em um NullReferenceException, porque você não pode acessar membros de um objeto que for nulo.

Para evitar essa exceção, você precisa verificar se minhaString é null antes de acessar seus membros, da seguinte forma:

string minhaString = null;

if (minhaString != null)
   Console.WriteLine(minhaString.Length);

ou

if (minhaString is not null)
    Console.WriteLine(minhaString.Length);
 

2- Comportamentos ou resultados não esperados

Se você não verificar valores nulos antes de usá-los, poderá acabar com um comportamento não intencional em seu aplicativo, como resultado ou comportamento incorreto.

Aqui está um exemplo de como o manuseio incorreto de valores nulos pode resultar em comportamento não intencional em C#:

string minhaString = null;

int tamanho = minhaString?.Length ?? 0;

// Saída: Tamanho: 0
Console.WriteLine(
"Tamanho: " + tamanho);

Neste exemplo, a variável minhaString recebe um valor nulo, mas usamos o operador de coalesência nula (??) para fornecer um valor padrão de 0 para a propriedade Length se ela for nula. Isso resulta em um comportamento indesejado, porque a propriedade Length de uma string nula não é 0, mas uma NullReferenceException seria gerada se tentássemos acessá-la diretamente.

Para evitar esse comportamento não intencional, é importante entender o comportamento dos valores nulos em seu código e tratá-los adequadamente. Nesse caso, uma abordagem melhor pode ser verificar se minhaString é null antes de acessar sua propriedade Length ou fornecer um valor padrão que faça sentido para seu caso de uso específico.

3- Impacto no desempenho

O tratamento inadequado de valores nulos também pode levar a problemas de desempenho, pois seu código pode ter que realizar verificações adicionais ou lidar com condições inesperadas.

Aqui está um exemplo de como o manuseio incorreto de valores nulos pode resultar em um impacto no desempenho em C#:

Aqui poderíamos ter usado a instrução if-else :

string minhaString = null;

for (int i = 0; i < 100000000; i++)
{
 
if (minhaString != null)
  {
   
int tamanho = minhaString.Length;
  }
}

Neste exemplo, temos um laço que itera 100 milhões de vezes, e dentro do laço, verificamos se minhaString é null antes de acessar sua propriedade Length. No entanto, como minhaString é nula, essa verificação é executada 100 milhões de vezes sem fazer nada de útil. Isso resulta em um impacto significativo no desempenho, pois o programa gasta muito tempo verificando nulo sem realmente fazer nenhum trabalho.

Para evitar esse impacto no desempenho, é importante entender o comportamento dos valores nulos em seu código e tratá-los adequadamente. Nesse caso, uma abordagem melhor pode ser atribuir um valor não nulo a minhaString para que a verificação dentro do loop não seja necessária ou estruturar o código para que a verificação seja realizada apenas uma vez e o resultado seja armazenado em uma variável para uso posterior.

4- Complexidade do código

O tratamento inconsistente de valores nulos pode tornar seu código mais complexo e difícil de manter, especialmente se exigir várias verificações de valores nulos em vários pontos do código.

Aqui está um exemplo de como o manuseio incorreto de valores nulos pode resultar em complexidade de código:

List<string> minhaLista = null;

if (minhaLista is not null)
{
 
for (int i = 0; i < minhaLista.Count; i++)
  {
    
string item = minhaLista[i];
    
if (item != null)
     {
      
// código
     }
  }
}

Neste exemplo, temos uma lista de strings e queremos executar alguma ação em cada item da lista, caso não seja nulo. No entanto, o código tornou-se complexo devido às várias verificações de valores nulos. Isso resulta em um risco maior de bugs, além de tornar o código mais difícil de ler e manter.

Para evitar essa complexidade, é importante entender o comportamento dos valores nulos em seu código e tratá-los adequadamente. Nesse caso, uma abordagem melhor pode ser usar um laço foreach e o operador condicional nulo (?.) para simplificar o código e torná-lo mais fácil de ler e manter:

 foreach (string item in minhaLista?.Where(x => x != null))
 {
   
//código
 }

Esse código é mais conciso, mais fácil de ler e menos sujeito a bugs, pois evita a necessidade de várias verificações de valores nulos.

Desde seu lançamento inicial, o C# introduziu vários novos recursos para facilitar o tratamento de problemas relacionados a valores nulos. Vejamos alguns destes recursos.

Operador de coalescência nula (??)

Esse operador permite fornecer um valor padrão para um tipo de referência nulo, facilitando o tratamento de casos em que um tipo de referência pode ser nulo. Exemplo:

Aluno aluno = new Aluno { Nome = "Maria" };

string
nome = aluno.Nome ?? "Desconhecido";
int
idade = aluno.Idade ?? 0;

Console.WriteLine($"Nome: {nome}, Idade: {idade}");
Console.ReadKey();

class Aluno
{
 
public string Nome { get; set; }
 
public int? Idade { get; set; }
}

Neste exemplo, temos uma classe User com uma propriedade Nome e uma propriedade Idade que pode ser anulada. Quando criamos uma instância da classe Aluno, apenas definimos a propriedade Nome, mas deixamos a propriedade Idade sem atribuição, portanto, ela é nula.

Em seguida, usamos o operador de união nula para fornecer valores padrão para as propriedades Nome e Idade. Se a propriedade Nome for nula, definimos como "Desconhecido". Se a propriedade Idade for nula, definimos como 0.

O resultado é que o valor de nome é "Maria" e o valor de Idade é 0. Isso permite lidar com valores nulos de maneira concisa e legível, sem ter que escrever várias verificações para valores nulos.

Operador condicional nulo (?.)

O operador condicional nulo (?.)  fornece uma maneira concisa de acessar uma propriedade ou método de um objeto, sem a necessidade de verificar valores nulos primeiro. Exemplo:

class Aluno
{
 
public string Nome { get; set; }
 
public Endereco Endereco { get; set; }
}

class Endereco
{
 
public string Local { get; set; }
}

Aluno aluno = new Aluno { Nome = "Maria" };

string local = aluno?.Endereco?.Local;

Console.WriteLine(local ?? "Desconhecido");
 

Aqui, temos uma classe Aluno com uma propriedade Nome e uma propriedade Endereco. Quando criamos uma instância da classe Aluno , apenas definimos a propriedade Nome, mas deixamos a propriedade Endereco sem atribuição, portanto, é nula.

Em seguida, usamos o operador condicional nulo para acessar a propriedade Local do objeto Endereco, sem precisar verificar valores nulos primeiro. Se aluno ou aluno.Endereco for nulo, o resultado da expressão será nulo e o ?? operador é usado para fornecer um valor padrão de "Desconhecido".

O resultado é que o valor de local é nulo e a saída é "Desconhecido". Isso permite acessar propriedades e métodos de um objeto sem ter que escrever várias verificações para valores nulos e reduz a complexidade do código.

Tipos de referência não anuláveis

No C# 8.0, foram introduzidos tipos de referência não anuláveis, que permitem especificar que um tipo de referência não deve ser nulo quando for usado. Depois de habilitar esse recurso, todos os tipos de referência são não anuláveis por padrão, a menos que você especifique explicitamente que é anulável.

Isso ajuda a evitar que NullReferenceExceptions ocorra em seu código. Aqui está um exemplo de uso de tipos de referência não anuláveis:

Aluno aluno = new Aluno();

//sem alerta do compilador
string
nome = aluno.Nome;

// requer a explicida verificação da não nulidade
Endereco endereco = aluno.Endereco!;

Console.WriteLine($"Nome: {nome}, Local: {endereco.Local}");

Console.ReadKey();

class Aluno
{
 
public string Nome { get; set; } = string.Empty;
 
public Endereco? Endereco { get; set; }
}

class Endereco
{
  
public string Local { get; set; } = string.Empty;
}

Neste exemplo, temos uma classe Aluno com uma propriedade Nome e uma propriedade Endereco. A propriedade Nome é declarada com um tipo de referência não anulável e é inicializada com uma string vazia. A propriedade Endereco é declarada com um tipo de referência anulável e não é inicializada, portanto, é nula por padrão.

Em seguida, criamos uma instância da classe Aluno e a atribuímos à variável aluno. Quando acessamos a propriedade Nome, o compilador não emite um alerta, pois é um tipo de referência não anulável e é inicializado.

No entanto, quando acessamos a propriedade Endereco, precisamos usar o operador tolerância nula (!) para verificar explicitamente se não é nula, ou o compilador emitirá um aviso. Isso garante que estejamos cientes do valor nulo potencial e evita comportamento não intencional.

O resultado é que o valor de nome é uma string vazia e o valor de endereco é nulo. Isso nos permite garantir que as variáveis de tipo de referência não sejam nulas, a menos que explicitamente permitidas, e reduz a probabilidade de NullReferenceException.

O recurso de tipos de referência não anuláveis é ativado por padrão após o .NET 6. Você pode desativá-lo no nível do projeto adicionando a configuração de projeto <Nullable>disable</Nullable>. Em um único arquivo de origem C#, você pode adicionar #nullable disablepragma para desabilitar o contexto anulável

Operador que tolerância nula (!)

O operador de tolerância nula (!)  é usado para suprimir a verificação nula ao acessar um tipo de referência anulável. Esse operador permite que você acesse uma propriedade ou chame um método de um tipo de referência anulável sem primeiro verificar se ele é nulo.

Aluno aluno = new Aluno();
Endereco? endereco = aluno.Endereco;

if (endereco != null)
{
  Console.WriteLine(
$"Local: {endereco.Local}");
}

//Usando o null-forgiving operator
//lança System.NullReferenceException pois
//Endereco é null

Console.WriteLine(
$"Local: {aluno.Endereco!.Local}");

Console.ReadKey();

class Aluno
{
  
public string Nome { get; set; } = string.Empty;
  
public Endereco? Endereco { get; set; }
}

class
Endereco
{
 
public string Local { get; set; } = string.Empty;
}

Neste exemplo, temos uma classe Aluno com uma propriedade Nome e uma propriedade Endereco. A propriedade Endereco é declarada como um tipo de referência anulável e não é inicializada, portanto, é nula por padrão.

Em seguida, criamos uma instância da classe Aluno e a atribuímos à variável aluno. Acessamos a propriedade Endereco e atribuímos à variável endereco.

Em seguida, verificamos se o endereço é nulo e, se não for, acessamos a propriedade Local.
 
Por fim, usamos o operador de tolerância nula para acessar a propriedade Local da propriedade Endereco diretamente, sem verificar se há nulo. Se Endereco for nulo, um System.NullReferenceException será lançado em tempo de execução.

O operador de tolerância nulo pode ser útil em certos casos, como quando você tem certeza de que um tipo de referência não é nulo ou quando deseja lançar uma exceção se for nulo. No entanto, geralmente é recomendável usar operadores de união nula ou tipos de referência não anuláveis para lidar com valores nulos, pois essas abordagens são mais seguras e explícitas.

Esses recursos, juntamente com o restante da linguagem e do runtime, fornecem um conjunto de ferramentas poderosas e flexíveis para lidar com valores nulos em C#.

E estamos conversados...

"Então Jesus disse: "Quando vocês levantarem o Filho do homem, saberão que Eu Sou, e que nada faço de mim mesmo, mas falo exatamente o que o Pai me ensinou."
João 8:28

Referências:


José Carlos Macoratti