Logotipo do ConstructiCat
CodeBust.
Explorar a seção ▾

Número mágico.

Um número mágico é um literal sem nome no código cujo sentido o leitor precisa adivinhar; uma constante nomeada deixa o código mais claro e seguro de alterar.

Em programação, um número mágico é um literal sem nome — geralmente um número, às vezes uma string — escrito diretamente no código-fonte e com um significado especial que não fica claro para quem lê. Como o valor não tem nome nem explicação, o leitor precisa adivinhar o que ele representa, e para alterá-lo com segurança é preciso encontrar todos os lugares onde ele foi repetido.

O termo também tem outros sentidos: uma constante gravada no início de um arquivo para identificar seu formato (uma assinatura de arquivo, como %PDF no início de documentos PDF) e um valor reconhecível, como 0xDEADBEEF, escrito na memória para facilitar a depuração. Esses usos não são antipadrões e estão fora do escopo deste artigo.

Constantes numéricas sem nome

Um valor assim também é chamado de constante mágica. Usá-lo é considerado um antipadrão e quebra uma das regras mais antigas da programação, que remonta aos manuais de COBOL, FORTRAN e PL/1 da década de 1960.

Por exemplo, no código a seguir, que calcula um preço após impostos, 1.05 é um número mágico, já que o valor codifica a alíquota do imposto sobre vendas, 5%, de uma forma pouco óbvia.

price_after_tax = 1.05 * price

O uso de números mágicos no código obscurece a intenção dos desenvolvedores ao escolher aquele número, aumenta as chances de erros sutis e torna mais difícil adaptar e estender o programa no futuro. Como exemplo, é difícil saber se cada dígito em 3.14159265358979323846 foi digitado corretamente, ou se essa constante de pi pode ser truncada para 3.14159 sem afetar a funcionalidade do programa com sua precisão reduzida. Substituir todos os números mágicos significativos por constantes nomeadas (também chamadas de variáveis explicativas) torna os programas mais fáceis de ler, entender e manter.

O exemplo acima pode ser melhorado adicionando uma variável com nome descritivo:

TAX = 0.05
price_after_tax = (1.0 + TAX) * price

Um bom nome pode resultar em código mais facilmente compreendido por um mantenedor que não seja o autor original, e até pelo próprio autor original depois de algum tempo. Um exemplo de constante com nome pouco informativo é int SIXTEEN = 16, enquanto int NUMBER_OF_BITS = 16 pode ser mais útil.

Dados não numéricos podem ter as mesmas propriedades mágicas e, portanto, os mesmos problemas dos números mágicos. Assim, declarar const string testUserName = "John" e usar testUserName pode ser melhor do que usar o literal "John" diretamente.

Exemplo: embaralhar um baralho

Por exemplo, se for necessário embaralhar aleatoriamente os valores em um vetor que representa um baralho padrão de cartas de baralho, este pseudocódigo realiza a tarefa usando o algoritmo de embaralhamento Fisher–Yates:

for i from 1 to 52
    j := i + randomInt(53 - i) - 1
    a.swapEntries(i, j)

onde a é um objeto vetor, a função randomInt(x) escolhe um inteiro aleatório entre 1 e x, inclusive, e swapEntries(i, j) troca a i-ésima e a j-ésima entradas do vetor. No exemplo anterior, 52 e 53 são números mágicos, e além disso não têm relação clara entre si. É considerado melhor estilo de programação escrever o seguinte:

int deckSize:= 52
for i from 1 to deckSize
    j := i + randomInt(deckSize + 1 - i) - 1
    a.swapEntries(i, j)

Isso é preferível por vários motivos:

  • Melhor legibilidade. Um programador que lê o primeiro exemplo pode se perguntar: O que o número 52 significa aqui? Por que 52? O programador talvez consiga inferir o significado após ler o código com atenção, mas isso não é óbvio. Os números mágicos tornam-se particularmente confusos quando o mesmo número é usado para finalidades diferentes em uma mesma seção do código.
  • Mais fácil de manter. É mais fácil alterar o valor do número, pois ele não está duplicado. Mudar o valor de um número mágico é propenso a erros, porque o mesmo valor costuma ser usado várias vezes em diferentes lugares do programa. Além disso, quando duas variáveis ou números semanticamente distintos têm o mesmo valor, eles podem ser acidentalmente editados juntos. Para modificar o primeiro exemplo de modo a embaralhar um baralho de Tarô, que tem 78 cartas, um programador poderia ingenuamente substituir cada ocorrência de 52 no programa por 78. Isso causaria dois problemas. Primeiro, deixaria passar o valor 53 na segunda linha do exemplo, o que faria o algoritmo falhar de forma sutil. Segundo, provavelmente substituiria os caracteres "52" em todos os lugares, independentemente de se referirem ao tamanho do baralho ou a algo completamente diferente, como o número de semanas em um ano do calendário gregoriano, ou, de forma mais traiçoeira, fazerem parte de um número como "1523", o que tudo introduziria bugs. Em contraste, mudar o valor da variável deckSize no segundo exemplo seria uma alteração simples, de uma única linha.
  • Incentiva a documentação. O único lugar onde a variável nomeada é declarada é um bom lugar para documentar o que o valor significa e por que ele tem esse valor. Ter o mesmo valor em uma infinidade de lugares ou leva a comentários duplicados (e aos problemas decorrentes de atualizar alguns e esquecer outros) ou não deixa nenhum único lugar onde seja ao mesmo tempo natural para o autor explicar o valor e provável que o leitor procure por uma explicação.
  • Agrupa informações. As declarações das variáveis de "números mágicos" podem ser colocadas juntas, geralmente no início de uma função ou arquivo, facilitando sua revisão e alteração.
  • Detecta erros de digitação. Usar uma variável (em vez de um literal) aproveita a verificação do compilador. Digitar acidentalmente "62" em vez de "52" passaria despercebido, ao passo que digitar "dekSize" em vez de "deckSize" faria o compilador avisar que dekSize não foi declarada.
  • Reduz a digitação. Se uma IDE oferecer suporte a autocompletar código, ela preencherá a maior parte do nome da variável a partir das primeiras letras.
  • Facilita a parametrização. Por exemplo, para generalizar o exemplo acima em um procedimento que embaralha um baralho com qualquer número de cartas, bastaria transformar deckSize em um parâmetro desse procedimento, ao passo que o primeiro exemplo exigiria várias alterações.
function shuffle (int deckSize)
   for i from 1 to deckSize
       j := i + randomInt(deckSize + 1 - i) - 1
       a.swapEntries(i, j)

As desvantagens são:

  • Quebra a localidade. Quando a constante nomeada não é definida perto de seu uso, isso prejudica a localidade e, portanto, a compreensibilidade do código. Colocar o 52 em um lugar possivelmente distante significa que, para entender completamente o funcionamento do laço "for" (por exemplo, para estimar o tempo de execução do laço), é preciso localizar a definição e verificar se é o número esperado. Isso é fácil de evitar (realocando a declaração) quando a constante é usada em apenas uma parte do código. Por outro lado, quando a constante nomeada é usada em partes distintas, a localização remota é uma pista para o leitor de que o mesmo valor aparece em outros lugares do código, que também podem valer a pena ser investigados.
  • Causa verbosidade. A declaração da constante acrescenta uma linha. Quando o nome da constante é mais longo do que o valor, em especial se várias dessas constantes aparecem em uma linha, pode ser necessário dividir uma única instrução lógica do código em várias linhas. Um aumento de verbosidade pode se justificar quando há alguma probabilidade de confusão sobre a constante, ou quando há probabilidade de a constante precisar ser alterada, como o reaproveitamento de uma rotina de embaralhamento para outros jogos de cartas. Pode igualmente se justificar como um aumento de expressividade.
  • Considerações de desempenho. Processar a expressão deckSize + 1 em tempo de execução pode ser mais lento do que o valor "53". Dito isso, a maioria dos compiladores modernos usa técnicas como dobra de constantes e otimização de laços para resolver a adição durante a compilação, de modo que normalmente não há penalidade de velocidade, ou ela é desprezível, em comparação ao uso de números mágicos no código. Sobretudo o custo de depuração e o tempo necessário para tentar entender um código não explicativo devem ser pesados contra o ínfimo custo de cálculo.

Uso aceito

Quando um literal numérico carece de significado especial, seu uso não é classificado como mágico, embora o que constitui "especial" seja subjetivo. Exemplos de literais que com frequência não são considerados mágicos incluem:

  • Uso de 0 e 1 como valores iniciais ou de incremento em um laço for, como em for (int i = 0; i < max; i += 1)
  • Uso do 2 para verificar se um número é par ou ímpar, como em isEven = (x % 2 == 0), onde % é o operador módulo
  • Uso de literais simples, p. ex., em expressões como circumference = 2 * Math.PI * radius, ou para calcular o discriminante de uma equação quadrática como d = b^2 − 4*a*c
  • Uso de potências de 10 para converter valores métricos (p. ex. entre gramas e quilogramas) ou para calcular valores de porcentagem e por mil
  • Expoentes em expressões como (f(x) ** 2 + f(y) ** 2) ** 0.5 para √(f(x)² + f(y)²)
  • Os literais 1 e 0 são às vezes usados para representar os valores booleanos verdadeiro e falso. É discutível, mas atribuir esses valores a nomes como TRUE e FALSE pode ser melhor.
  • Em C e C++, o 0 é frequentemente usado para significar ponteiro nulo, embora a biblioteca padrão de C defina uma macro NULL e o C++ moderno inclua a palavra-chave nullptr.

Exemplo

No método abaixo, 1.2, 30 e 0.015 codificam regras de negócio — o imposto, um prazo de carência e uma multa por atraso — e o leitor precisa adivinhar qual é qual. Pior: 30 aparece duas vezes, com dois significados diferentes.

class Billing {
    static double total(double net, int daysLate) {
        double total = net * 1.2;
        if (daysLate > 30) {
            total += total * 0.015 * (daysLate / 30);
        }
        return total;
    }
}

Agora cada valor tem um nome, e o prazo de carência e o período de faturamento podem mudar de forma independente (veja Substituir Número Mágico por Constante Simbólica).

class Billing {
    static final double VAT_RATE = 0.20;
    static final int GRACE_PERIOD_DAYS = 30;
    static final int DAYS_PER_BILLING_PERIOD = 30;
    static final double LATE_FEE_RATE_PER_PERIOD = 0.015;

    static double total(double net, int daysLate) {
        double total = net * (1 + VAT_RATE);
        if (daysLate > GRACE_PERIOD_DAYS) {
            int overduePeriods = daysLate / DAYS_PER_BILLING_PERIOD;
            total += total * LATE_FEE_RATE_PER_PERIOD * overduePeriods;
        }
        return total;
    }
}

Adaptado do artigo da Wikipédia “Magic number (programming)” (revisão 1355218563), disponível sob a licença CC BY-SA 4.0; o texto foi traduzido e editado, e exemplos foram adicionados.