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
- Tipos de referência anuláveis — a explicação oficial do recurso moderno de segurança contra nulos; a base desta aula.
- O operador de acesso condicional (?.) — como acessar membros com segurança contra nulos.
- O operador de coalescência nula (??) — como fornecer valores padrão para nulos.
- NullReferenceException — o erro de execução mais comum e como evitá-lo.
- Tipos de valor anuláveis — como números e outros tipos por valor também podem ser anuláveis (
int?).
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.