Armadilhas Comuns: O Mapa dos Perigos

Armadilhas Comuns: O Mapa dos Perigos

As seis armadilhas que pegam todo programador C#: confundir atribuição com comparação, a contagem que começa em zero, o uso de algo nulo, a divisão inteira inesperada, modificar uma coleção durante o percurso e esquecer que objetos são compartilhados por referência.
Linguagem C#

• • 10 min de leitura

Todo programador, por mais cuidadoso, tropeça em certos erros com frequência — armadilhas que a linguagem e a natureza da programação armam para os desavisados. Muitas delas você provavelmente já encontrou ao longo do curso, talvez perdendo minutos ou horas até descobrir a causa. Esta aula reúne as mais comuns num só lugar, explicando por que acontecem e como evitá-las ou reconhecê-las rapidamente. Considere-a um "mapa dos perigos": conhecer de antemão onde ficam as ciladas o pouparia de muitas frustrações e, mais importante, treinará seu olho para diagnosticar problemas depressa. Nenhuma dessas armadilhas é nova para você em conceito — todas tocam temas que estudamos —, mas vê-las reunidas e nomeadas como armadilhas dá a você uma vantagem prática valiosa. Um bom programador não é quem nunca erra, mas quem reconhece e corrige seus erros com rapidez.

Armadilha 1: confundir atribuição (=) com comparação (==)

Esta é talvez a confusão mais clássica de todas, e vale reforçá-la. O sinal = (um só) atribui — guarda um valor numa variável. O sinal == (dois) compara — verifica se dois valores são iguais, produzindo verdadeiro ou falso.

int idade = 30;   // um '=': ATRIBUI 30 a idade
if (idade == 30)  // dois '==': COMPARA se idade é 30

O engano é escrever if (idade = 30) (com um só) querendo comparar. Felizmente, na maioria dos casos, o C# o protege: como a condição de um if precisa ser um verdadeiro/falso, e idade = 30 produz um número (não um bool), o compilador acusa erro. Mas o hábito de distinguir "um igual guarda, dois iguais comparam" evita a confusão na origem. Sempre que for comparar, use ==; sempre que for guardar, use =.

Armadilha 2: a contagem que começa em zero

Índices de coleções (arrays, listas) começam em zero, não em um. O primeiro elemento está na posição 0, e o último está na posição Tamanho - 1, não Tamanho.

int[] numeros = { 10, 20, 30 };   // três elementos: posições 0, 1, 2
Console.WriteLine(numeros[0]);    // 10 — o PRIMEIRO
// Console.WriteLine(numeros[3]); // QUEBRA! posição 3 não existe (índices vão de 0 a 2)

O erro típico é tentar acessar numeros[3] num array de três elementos, pensando no "terceiro" ou esquecendo que a contagem começa do zero — o que quebra o programa com um erro de "índice fora do intervalo". A regra a fixar: num conjunto de N elementos, os índices válidos vão de 0 a N-1. Quando encontrar esse erro, sua primeira suspeita deve ser um acesso que ultrapassou o limite por causa da contagem a partir do zero.

Armadilha 3: usar algo que é nulo

Tentar acessar uma property ou método de uma variável que é null quebra o programa com a temida exceção de referência nula. É o erro de tratar o "nada" como se fosse "algo".

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

Este é um dos erros mais comuns em tempo de execução. A prevenção: verifique se algo pode ser nulo antes de usá-lo (com if (x != null), ou com os operadores ?. e ??), e preste atenção aos avisos do compilador moderno sobre possíveis nulos. Quando encontrar uma "NullReferenceException", a pergunta a fazer é: "qual variável, naquela linha, estava valendo null quando eu tentei usá-la?".

Armadilha 4: a divisão inteira inesperada

Quando você divide dois números inteiros, o C# faz divisão inteira, descartando a parte fracionária — o que surpreende quem espera um resultado com casas decimais.

int a = 10, b = 3;
double resultado = a / b;   // resultado é 3.0, não 3.333! (a/b é feito como inteiros)

O engano é esperar 3.333 e obter 3, porque a divisão aconteceu entre inteiros antes de o resultado ser guardado no double. A prevenção: se você quer casas decimais, garanta que ao menos um dos números seja de tipo decimal — por exemplo, (double)a / b, convertendo a para double antes da divisão. Quando um cálculo com divisão der um resultado "redondo" inesperado, suspeite da divisão inteira.

Armadilha 5: modificar uma coleção enquanto a percorre

Tentar adicionar ou remover elementos de uma coleção enquanto você a percorre com foreach causa um erro de execução. O foreach não permite que a coleção mude sob seus pés durante a iteração.

List<int> numeros = new() { 1, 2, 3, 4 };
// foreach (int n in numeros)
//     if (n % 2 == 0) numeros.Remove(n);   // QUEBRA! modificando durante o foreach

A prevenção: se você precisa remover itens enquanto examina uma coleção, use um laço for com índice (com cuidado), ou colete os itens a remover numa lista separada e remova-os depois do foreach, ou use métodos apropriados como RemoveAll. Reconheça o erro "coleção foi modificada" como sinal desta armadilha.

Armadilha 6: esquecer que objetos são compartilhados por referência

Como vimos na aula de valor e referência, atribuir uma variável de objeto a outra não copia o objeto — copia a referência, fazendo as duas apontarem para o mesmo objeto. Alterar um afeta o outro.

var lista1 = new List<int> { 1, 2, 3 };
var lista2 = lista1;      // NÃO é uma cópia! lista2 aponta para a MESMA lista
lista2.Add(4);
Console.WriteLine(lista1.Count);   // 4! lista1 mudou também

O engano é achar que lista2 é uma cópia independente e surpreender-se quando alterar lista2 muda lista1. A prevenção: lembre que objetos são compartilhados por referência; se precisar de uma cópia independente, crie um novo objeto explicitamente. Quando uma alteração "inexplicável" aparecer num objeto que você não tocou diretamente, suspeite de referências compartilhadas.

Um princípio geral: leia a mensagem de erro

Reunidas essas armadilhas, vale reforçar o princípio que as atravessa e que já enfatizamos na aula de depuração: a mensagem de erro é sua aliada. Cada uma dessas armadilhas, quando cai nela, produz uma mensagem — "índice fora do intervalo", "referência nula", "coleção modificada" — que aponta diretamente para a natureza do problema. O programador experiente não decorou soluções; ele aprendeu a ler as mensagens e a associá-las às causas típicas, o que esta aula acabou de ajudá-lo a fazer. Quando um programa quebrar, não entre em pânico nem mude coisas ao acaso: leia a mensagem, identifique o tipo de erro, e cruze-o com o mapa de armadilhas que você agora conhece. Na maioria das vezes, isso o leva direto à causa. Diagnosticar bem é metade de corrigir, e você acabou de ganhar um bom mapa para diagnosticar.

Seis armadilhas ficaram mapeadas, e todas elas compartilham uma característica: são erros que qualquer programador comete, não sinais de despreparo. Confundir atribuição com comparação, esquecer que a contagem começa em zero, usar algo nulo, ser surpreendido pela divisão inteira, modificar uma coleção durante o percurso e ignorar que objetos são compartilhados por referência.

O valor de conhecê-las antecipadamente está no tempo de diagnóstico: um sintoma que levaria uma tarde de perplexidade passa a ser reconhecido em segundos. E o princípio geral vale mais que a lista inteira — leia a mensagem de erro. Ela quase sempre aponta o arquivo, a linha e a natureza do problema.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Provoque deliberadamente a armadilha 2 (índice fora do intervalo): crie um array de três elementos e tente acessar a posição 3. Leia a mensagem de erro e explique como ela aponta para a causa. Depois, corrija o acesso para uma posição válida.

Ver resposta

✓ Resposta: Exemplo:

int[] arr = { 10, 20, 30 };
// Console.WriteLine(arr[3]);   // erro
Console.WriteLine(arr[2]);      // correto: 30 (última posição válida)

Ao acessar arr[3], o programa quebra com uma mensagem como "Index was outside the bounds of the array" ("índice estava fora dos limites do array"), que aponta diretamente para a causa: tentou-se acessar uma posição que não existe. Como o array tem três elementos, as posições válidas vão de 0 a 2, e a correção é usar um índice dentro desse intervalo (por exemplo, arr[2] para o último elemento).

Exercício 2

Escreva um trecho que caia na armadilha 4 (divisão inteira): divida dois inteiros esperando um resultado decimal, e observe o resultado "redondo". Depois, corrija-o para obter o resultado com casas decimais, e explique o que sua correção fez.

Ver resposta

✓ Resposta: Exemplo:

int a = 7, b = 2;
double errado = a / b;          // 3.0 (divisão inteira: 7/2 = 3, descartando a fração)
double certo = (double)a / b;   // 3.5

O primeiro resultado é 3.0 porque a / b é calculado entre inteiros antes de ser guardado no double, descartando a parte fracionária. A correção (double)a / b converte a para double antes da divisão, fazendo o C# realizar uma divisão decimal (não inteira), o que preserva a fração e produz 3.5. Basta um dos operandos ser decimal para a divisão inteira ser evitada.

Exercício 3

Reproduza a armadilha 6 (referência compartilhada): crie uma List, atribua-a a outra variável, altere a segunda, e mostre que a primeira mudou também. Explique por que isso acontece e como você obteria uma cópia de verdade.

Ver resposta

✓ Resposta: Exemplo:

var listaA = new List<int> { 1, 2, 3 };
var listaB = listaA;   // não copia; aponta para a mesma lista
listaB.Add(4);
Console.WriteLine(listaA.Count);   // 4 — mudou também

Isso acontece porque List é um tipo por referência: listaB = listaA copia apenas a referência (o endereço), fazendo listaA e listaB apontarem para o mesmo objeto na memória; alterar por uma afeta o que a outra vê, pois há uma só lista. Para obter uma cópia de verdade, seria preciso criar uma nova lista com os mesmos elementos (por exemplo, var copia = new List<int>(listaA);), gerando um objeto independente.

Exercício 4

Para cada mensagem de erro a seguir, diga a qual armadilha desta aula ela provavelmente corresponde e qual seria sua primeira suspeita sobre a causa: (a) "Index was outside the bounds of the array"; (b) "Object reference not set to an instance of an object" (a mensagem da referência nula); (c) "Collection was modified; enumeration operation may not execute".

Ver resposta

✓ Resposta: (a) "Index was outside the bounds of the array" → armadilha 2 (contagem/índice); primeira suspeita: um acesso a uma posição de array ou lista que ultrapassou o limite, provavelmente por esquecer que a contagem começa em zero (o último índice é Tamanho - 1). (b) "Object reference not set to an instance of an object" → armadilha 3 (nulo); primeira suspeita: alguma variável estava valendo null naquela linha quando se tentou acessar uma property ou método dela. (c) "Collection was modified; enumeration operation may not execute" → armadilha 5 (modificar durante o foreach); primeira suspeita: um Add ou Remove na coleção dentro de um foreach que a percorre.

Exercício 5

Sem código: a aula afirma que "diagnosticar bem é metade de corrigir". Explique como conhecer as armadilhas comuns e saber ler as mensagens de erro torna você mais rápido em resolver problemas, em comparação com mudar o código ao acaso quando algo quebra.

Ver resposta

✓ Resposta: "Diagnosticar bem é metade de corrigir" significa que a maior parte do esforço de resolver um bug está em identificar a causa; uma vez conhecida a causa, a correção costuma ser simples. Conhecer as armadilhas comuns e saber ler as mensagens de erro torna você mais rápido porque, quando um programa quebra, você associa imediatamente a mensagem ("índice fora do intervalo", "referência nula") ao tipo de problema e à sua causa típica, indo direto ao ponto — sabe onde olhar e o que suspeitar. Isso contrasta com mudar o código ao acaso: sem entender a causa, você tenta soluções cegas, uma após outra, o que é lento, frustrante, e pode até introduzir novos erros sem resolver o original. Diagnosticar a partir da mensagem transforma a correção de um processo de tentativa e erro num processo dirigido e eficiente.

Comentários

Mais em Linguagem C#

Classes e Campos: Criando seu Primeiro Molde
Classes e Campos: Criando seu Primeiro Molde

A classe como molde e o objeto como exemplar: como definir uma classe com seus…

Projeto: Um Sistema de Formas de Pagamento
Projeto: Um Sistema de Formas de Pagamento

Um projeto integrador que reúne toda a fase de objetos num sistema de formas…

Recursos Modernos: Pattern Matching, Tuplas e Records
Recursos Modernos: Pattern Matching, Tuplas e Records

Três recursos do C# moderno que encurtam o código do dia a dia: o pattern…