Nulos: Lidando com a Ausência de Valor

Nulos: Lidando com a Ausência de Valor

O erro mais comum da plataforma e como evitá-lo: o que é nulo, por que usar o nada como se fosse algo quebra o programa, e as defesas disponíveis — verificação explícita, o acesso condicional, o operador de coalescência e a proteção do compilador contra referências nulas.
Linguagem C#

• • 11 min de leitura

Na aula anterior, aprendemos a tratar erros depois que acontecem, com as exceções. Nesta, encaramos uma categoria específica de erro tão comum e custosa que ganhou fama própria na história da programação, e que o C# moderno oferece ferramentas para prevenir antes mesmo de o programa rodar. Trata-se dos erros causados por valores nulos — a ausência de um valor onde se esperava um objeto. Um nulo representa "nada", "vazio", "nenhum objeto"; e o problema surge quando tentamos usar esse "nada" como se fosse alguma coisa. É um erro tão frequente que o próprio criador do conceito de referência nula, décadas depois, chamou-o de seu "erro de um bilhão de dólares", pela quantidade de falhas que causou ao longo do tempo. Nesta aula, entenderemos o que é o nulo, o perigo que ele representa, e as ferramentas do C# — algumas modernas e elegantes — para lidar com ele com segurança.

O que é nulo

Lembre-se da distinção entre tipos por valor e por referência (Aula #33): uma variável de um tipo por referência (um objeto, uma lista, um texto) não guarda o objeto em si, mas uma referência — um endereço — que aponta para ele. Ora, e se essa referência não apontar para nada? Esse "apontar para nada" é o nulo, representado em C# pela palavra null. Uma variável de referência que vale null não aponta para nenhum objeto — é como o "papel com endereço" da Aula #32, mas em branco, sem endereço escrito.

string nome = null;   // 'nome' não aponta para nenhum texto — está "vazia"
Pessoa p = null;      // 'p' não aponta para nenhuma pessoa

O nulo é útil e legítimo: ele representa, de forma explícita, a ausência de um valor. Um método que busca uma pessoa por nome e não a encontra pode devolver null para dizer "não achei ninguém". Um campo que ainda não foi preenchido pode ser null. O nulo, em si, não é o problema — o problema é o que acontece quando tentamos usar um nulo como se ele fosse um objeto de verdade.

O perigo: usar o nada como se fosse algo

Aqui está o cerne do problema. Se uma variável é null (não aponta para nada) e você tenta acessar algo dentro dela — uma property, um método —, o programa quebra, pois não há objeto ali para acessar. É como tentar ler o tamanho de um texto que não existe:

string nome = null;
Console.WriteLine(nome.Length);   // QUEBRA! 'nome' é null, não há texto para medir

Essa quebra é uma exceção específica, chamada, em inglês, NullReferenceException ("exceção de referência nula") — provavelmente o erro em tempo de execução mais comum da história da programação. Ela acontece toda vez que se tenta acessar um membro de algo que é null. O drama é que esse perigo costuma ser invisível até o programa quebrar: nada, à primeira vista, avisa que nome poderia ser nulo. Você escreve nome.Length confiante, e só descobre o problema quando o programa estoura durante a execução — talvez em produção, diante de um usuário. Prevenir esse erro é o objetivo desta aula.

Verificando antes de usar

A forma mais fundamental de se proteger é verificar se uma variável é nula antes de usá-la, com um simples if:

string nome = ObterNome();   // pode devolver um texto ou null

if (nome != null)
{
    Console.WriteLine(nome.Length);   // seguro: só acessamos se não for null
}
else
{
    Console.WriteLine("Nome não informado.");
}

Verificar nome != null antes de acessar nome.Length elimina o risco: só entramos no bloco que usa o texto se ele de fato existir. Essa verificação explícita é sempre válida e é a base de tudo. Mas escrevê-la repetidamente pode ser verboso, então o C# oferece operadores concisos que tornam o trato com nulos mais elegante.

Os operadores de nulo: ?. e ??

O C# oferece dois operadores especialmente úteis para lidar com nulos de forma compacta. O primeiro é o operador de acesso condicional, escrito ?. (uma interrogação antes do ponto). Ele acessa um membro apenas se o objeto não for nulo; se for nulo, a expressão inteira resulta em nulo, sem quebrar:

string nome = null;

// Com '?.': se 'nome' for null, o resultado é null; senão, é o Length.
int? tamanho = nome?.Length;   // NÃO quebra — 'tamanho' fica null
Console.WriteLine(tamanho);    // (nada, pois é null)

Leia nome?.Length como "o Length de nome, se nome não for nulo; senão, nulo". O ?. verifica a nulidade automaticamente antes de acessar, evitando a quebra. (O tipo do resultado é int?, com uma interrogação — um "inteiro que pode ser nulo" —, pois o resultado pode ser um número ou nulo; falaremos disso adiante.)

O segundo é o operador de coalescência nula, escrito ?? (duas interrogações). Ele fornece um valor alternativo para quando algo é nulo: a ?? b significa "use a, mas se a for nulo, use b no lugar":

string entrada = ObterEntrada();   // pode ser null

// Se 'entrada' for null, usa "Anônimo" no lugar:
string nomeGarantido = entrada ?? "Anônimo";
Console.WriteLine(nomeGarantido);   // nunca será null: ou a entrada, ou "Anônimo"

O ?? garante um valor não nulo: se entrada tiver um texto, ele é usado; se for nula, "Anônimo" a substitui. É perfeito para fornecer padrões seguros. Os dois operadores combinam-se elegantemente — entrada?.Length ?? 0 daria "o tamanho de entrada, ou 0 se entrada for nula". Esses operadores tornam o código que lida com nulos conciso e legível, sem abrir mão da segurança.

A proteção moderna: o compilador avisa

O C# moderno traz uma proteção ainda mais poderosa contra nulos: um recurso pelo qual o próprio compilador avisa você, antes de o programa rodar, sobre lugares onde um acesso a um valor possivelmente nulo poderia quebrar. Esse recurso — ativado por padrão em projetos novos — introduz uma distinção no próprio tipo: um texto que nunca deve ser nulo é declarado como string, enquanto um texto que pode ser nulo é declarado como string? (com uma interrogação):

string sempreTem = "Ana";     // não deve ser nulo
string? podeSerNulo = null;   // o '?' declara que ESTE pode ser nulo

// sempreTem = null;          // AVISO do compilador: atribuindo null a algo não anulável

Console.WriteLine(podeSerNulo.Length);   // AVISO: 'podeSerNulo' pode ser null aqui!

Repare na palavra "aviso": o compilador aponta o dedo para cada lugar onde uma quebra por nulo poderia acontecer, marcando-o na sua tela antes de você rodar o programa. É como ter um revisor atento destacando todos os pontos de risco. A interrogação (string?) é como você diz ao compilador "este valor pode legitimamente ser nulo, e eu vou tratá-lo com cuidado" — e o compilador então exige que você verifique antes de usá-lo. Para os valores declarados sem interrogação (string), o compilador confia que nunca serão nulos e avisa se você tentar torná-los nulos. Essa distinção no tipo transforma um erro que só apareceria durante a execução num aviso em tempo de compilação, permitindo corrigi-lo antes que vire um problema real. É mais um exemplo da filosofia do C# que vimos por todo o curso: detectar erros o mais cedo possível.

Não se preocupe em dominar todas as nuances deste recurso agora; o essencial é entender a ideia. O C# moderno o ajuda a evitar o "erro de um bilhão de dólares" fazendo o compilador vigiar, para você, onde os nulos podem causar problema. Quando você vir um aviso desses, não o ignore: ele está apontando um risco real, e a solução costuma ser verificar a nulidade (com if, ?. ou ??) antes de usar o valor.

A ausência de valor deixou de ser uma armadilha invisível. Nulo não é zero nem texto vazio: é a indicação explícita de que não há objeto algum ali, e tentar usar o nada como se fosse algo produz o erro mais comum da plataforma.

O repertório de defesa ficou completo: verificar antes de usar, encurtar a verificação com o acesso condicional, e fornecer um padrão com o operador de coalescência. Melhor ainda é a proteção moderna, em que o compilador distingue o que pode ser nulo do que não pode e avisa em tempo de compilação — transformando um erro que só apareceria diante do usuário num aviso que aparece enquanto se escreve.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva um programa que declare uma variável string valendo null e tente acessar .Length diretamente, observando a quebra (a NullReferenceException). Depois, proteja o acesso com uma verificação if (variavel != null) e confirme que o programa não quebra mais.

Ver resposta

✓ Resposta: Exemplo:

string texto = null;
// Console.WriteLine(texto.Length);   // QUEBRA: NullReferenceException
if (texto != null)
    Console.WriteLine(texto.Length);
else
    Console.WriteLine("Texto vazio.");

Acessar texto.Length diretamente quebra, pois texto é nulo e não há objeto para medir. A verificação if (texto != null) protege o acesso: só se entra no bloco que usa o .Length quando o texto de fato existe, evitando a quebra.

Exercício 2

Reescreva a proteção do exercício anterior usando o operador ?. para obter o tamanho (ou nulo se a variável for nula), e depois combine-o com o operador ?? para obter o tamanho ou 0 numa única expressão. Explique o que cada operador faz.

Ver resposta

✓ Resposta: Exemplo:

string texto = null;
int? tamanho = texto?.Length;          // null, sem quebrar
int tamanhoSeguro = texto?.Length ?? 0; // 0, pois texto é null

O operador ?. acessa .Length apenas se texto não for nulo; sendo nulo, a expressão resulta em nulo, sem quebrar. O operador ?? fornece um valor alternativo quando o lado esquerdo é nulo: como texto?.Length é nulo, o ?? 0 o substitui por 0. Combinados, produzem "o tamanho do texto, ou 0 se o texto for nulo", numa expressão segura e concisa.

Exercício 3

Escreva um método string ObterNomeOuPadrao(string? nome) que devolva o nome recebido, ou "Sem nome" caso ele seja nulo, usando o operador ??. Teste-o passando um nome válido e passando null.

Ver resposta

✓ Resposta: Exemplo:

string ObterNomeOuPadrao(string? nome)
{
    return nome ?? "Sem nome";
}
Console.WriteLine(ObterNomeOuPadrao("Ana"));   // Ana
Console.WriteLine(ObterNomeOuPadrao(null));    // Sem nome

O operador ?? devolve o nome recebido se ele não for nulo, ou "Sem nome" caso seja nulo — garantindo que o método sempre retorne um texto válido.

Exercício 4

Explique, com suas palavras, o que representa um valor null e por que ele, em si, não é um problema. Em que momento exato o nulo se torna a causa de um erro (a NullReferenceException)?

Ver resposta

✓ Resposta: Um valor null representa a ausência de um valor — uma variável de referência que não aponta para nenhum objeto, o "nada". Em si, ele não é um problema, pois é uma forma legítima e útil de expressar "não há valor" ou "não encontrado" (por exemplo, uma busca que não achou nada pode devolver null). O nulo se torna a causa de um erro no momento exato em que se tenta usá-lo como se fosse um objeto — acessar uma property ou chamar um método de uma variável que é nula. Como não há objeto ali, o acesso falha, gerando a NullReferenceException. O problema não é a existência do nulo, mas o uso indevido dele como se fosse algo concreto.

Exercício 5

Sem código: explique a diferença entre declarar uma variável como string e como string? no C# moderno. O que o compilador faz de diferente em cada caso, e como isso ajuda a prevenir erros de nulo antes de o programa rodar?

Ver resposta

✓ Resposta: Declarar uma variável como string (sem interrogação) indica que ela não deve ser nula — a intenção é que ela sempre tenha um valor. Declarar como string? (com interrogação) indica que ela pode legitimamente ser nula. No C# moderno, o compilador trata os dois casos de forma diferente: para uma string, ele avisa se você tentar atribuir null a ela (contrariando a intenção de não nulidade); para uma string?, ele avisa se você tentar usá-la (acessar seus membros) sem antes verificar que não é nula. Isso ajuda a prevenir erros de nulo antes de o programa rodar porque transforma um risco que só apareceria durante a execução (a quebra por nulo) num aviso em tempo de compilação, apontado na tela do programador, que pode então adicionar a verificação necessária e corrigir o problema antes que ele vire uma falha real.

Comentários

Mais em Linguagem C#

Métodos: Dando Nome e Reúso às Ações
Métodos: Dando Nome e Reúso às Ações

O primeiro recurso real de organização do código: o método, que dá nome a uma…

Revisão: A Jornada Até Aqui
Revisão: A Jornada Até Aqui

Uma revisão que reorganiza o curso inteiro: fundamentos, controle de fluxo…

Conversão de Tipos: Transformando Texto em Número, com Segurança
Conversão de Tipos: Transformando Texto em Número, com Segurança

Como transformar o texto que o usuário digita em números utilizáveis. A…