Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Магическое число.

Магическое число — безымянный литерал в коде, смысл которого читателю приходится угадывать; именованная константа делает код понятнее и безопаснее для изменений.

В программировании магическое число — это безымянный литерал (обычно число, иногда строка), который записан прямо в исходном коде и несёт особый смысл, неочевидный читателю. Поскольку значение не названо и не объяснено, читателю приходится догадываться, что оно означает, а чтобы безопасно его изменить, нужно найти все места, где оно повторяется.

У термина есть и другие значения: так называют константу в начале файла, по которой определяют его формат (сигнатуру файла — например, %PDF в начале PDF-документов), а также характерное значение вроде 0xDEADBEEF, которое записывают в память, чтобы упростить отладку. Такие применения антипаттернами не являются, и в этой статье они не рассматриваются.

Безымянные числовые константы

Такое значение называют также магической константой. Её использование считается антипаттерном и нарушает одно из старейших правил программирования, восходящее к руководствам по COBOL, FORTRAN и PL/1 1960-х годов.

Например, в следующем коде, вычисляющем цену с учётом налога, 1.05 является магическим числом, поскольку это значение неочевидным образом кодирует ставку налога с продаж, равную 5 %.

price_after_tax = 1.05 * price

Использование магических чисел в коде скрывает намерения разработчиков при выборе этого числа, увеличивает вероятность незаметных ошибок и затрудняет дальнейшую адаптацию и расширение программы. Например, трудно определить, верно ли набрана каждая цифра в 3.14159265358979323846, или же эту константу для числа пи можно усечь до 3.14159 без ущерба для работы программы при пониженной точности. Замена всех значимых магических чисел именованными константами (которые также называют поясняющими переменными) делает программы более простыми для чтения, понимания и сопровождения.

Приведённый выше пример можно улучшить, добавив переменную с описательным именем:

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

Удачное имя позволяет получить код, который легче понять сопровождающему разработчику, не являющемуся автором, и даже самому автору спустя некоторое время. Пример неинформативно названной константы — int SIXTEEN = 16, тогда как int NUMBER_OF_BITS = 16 может оказаться более полезным.

Нечисловые данные могут обладать теми же «магическими» свойствами, а значит, и теми же проблемами, что и магические числа. Так, объявление const string testUserName = "John" и использование testUserName может быть предпочтительнее, чем использование литерала "John" напрямую.

Пример: перетасовка колоды карт

Например, если требуется случайным образом перемешать значения в массиве, представляющем стандартную колоду игральных карт, этот псевдокод решает задачу с помощью алгоритма тасования Фишера — Йейтса:

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

где a — объект-массив, функция randomInt(x) выбирает случайное целое число от 1 до x включительно, а swapEntries(i, j) меняет местами i-й и j-й элементы массива. В предыдущем примере 52 и 53 — магические числа, к тому же явно не связанные друг с другом. Считается, что лучше писать так:

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

Это предпочтительнее по нескольким причинам:

  • Лучшая читаемость. Программист, читающий первый пример, может задаться вопросом: что здесь означает число 52? Почему 52? Он может понять смысл, внимательно прочитав код, но это неочевидно. Магические числа становятся особенно запутанными, когда одно и то же число используется для разных целей в одном фрагменте кода.
  • Проще в сопровождении. Изменить значение числа легче, поскольку оно не дублируется. Изменение значения магического числа чревато ошибками, потому что одно и то же значение часто используется несколько раз в разных местах программы. Кроме того, когда две семантически различные переменные или числа имеют одно и то же значение, их могут случайно отредактировать вместе. Чтобы изменить первый пример для тасования колоды Таро, в которой 78 карт, программист может наивно заменить каждое вхождение 52 в программе на 78. Это привело бы к двум проблемам. Во-первых, было бы пропущено значение 53 во второй строке примера, из-за чего алгоритм незаметно дал бы сбой. Во-вторых, скорее всего, символы «52» были бы заменены повсюду, независимо от того, обозначают ли они размер колоды или нечто совершенно иное, например число недель в григорианском календарном году, или, что ещё коварнее, являются частью числа вроде «1523», — и всё это внесло бы ошибки. Напротив, изменение значения переменной deckSize во втором примере было бы простым изменением в одну строку.
  • Поощряет документирование. Единственное место, где объявляется именованная переменная, — удачное место, чтобы задокументировать, что означает это значение и почему оно именно такое. Наличие одного и того же значения во множестве мест либо приводит к дублированию комментариев (и сопутствующим проблемам, когда часть из них обновляют, а часть упускают), либо не оставляет ни одного места, где автору было бы естественно объяснить значение и где читатель, вероятно, станет искать объяснение.
  • Объединяет информацию. Объявления переменных-«магических чисел» можно разместить вместе, обычно в начале функции или файла, что упрощает их просмотр и изменение.
  • Обнаруживает опечатки. Использование переменной (вместо литерала) задействует проверку компилятора. Случайно набранное «62» вместо «52» осталось бы незамеченным, тогда как набранное «dekSize» вместо «deckSize» привело бы к предупреждению компилятора о том, что dekSize не объявлена.
  • Сокращает набор текста. Если IDE поддерживает автодополнение кода, она подставит большую часть имени переменной по первым нескольким буквам.
  • Облегчает параметризацию. Например, чтобы обобщить приведённый выше пример в процедуру, тасующую колоду из любого числа карт, достаточно превратить deckSize в параметр этой процедуры, тогда как первый пример потребовал бы нескольких изменений.
function shuffle (int deckSize)
   for i from 1 to deckSize
       j := i + randomInt(deckSize + 1 - i) - 1
       a.swapEntries(i, j)

Недостатки:

  • Нарушает локальность. Когда именованная константа определена не рядом с местом её использования, это вредит локальности, а значит, и понятности кода. Размещение 52 в потенциально отдалённом месте означает, что для полного понимания работы цикла «for» (например, чтобы оценить время его выполнения) приходится разыскивать определение и проверять, что это ожидаемое число. Этого легко избежать (перенеся объявление), когда константа используется лишь в одной части кода. Когда же именованная константа используется в разрозненных частях, отдалённое местоположение служит читателю подсказкой, что то же значение встречается и в других местах кода, которые, возможно, тоже стоит изучить.
  • Приводит к многословию. Объявление константы добавляет строку. Когда имя константы длиннее её значения, особенно если несколько таких констант появляются в одной строке, может потребоваться разбить один логический оператор кода на несколько строк. Увеличение многословия может быть оправдано, когда есть вероятность путаницы относительно константы или когда константу, возможно, придётся изменить, например при повторном использовании процедуры тасования для других карточных игр. Оно может быть в равной мере оправдано как повышение выразительности.
  • Соображения производительности. Обработка выражения deckSize + 1 во время выполнения может быть медленнее, чем использование значения «53». Тем не менее большинство современных компиляторов применяют такие методы, как свёртка констант и оптимизация циклов, чтобы вычислить сложение при компиляции, поэтому по сравнению с использованием магических чисел в коде потери в скорости обычно отсутствуют или пренебрежимо малы. В особенности стоимость отладки и время, затрачиваемое на попытки понять не поясняющий код, нужно сопоставлять с ничтожной стоимостью вычисления.

Допустимое использование

Когда числовой литерал не несёт особого смысла, его использование не считается «магическим», хотя то, что считать особым, субъективно. Примеры литералов, которые часто не считают магическими:

  • Использование 0 и 1 в качестве начальных или приращиваемых значений в цикле for, например for (int i = 0; i < max; i += 1)
  • Использование 2 для проверки чётности числа, как в isEven = (x % 2 == 0), где % — оператор взятия остатка
  • Использование степеней 10 для перевода метрических величин (например, между граммами и килограммами) или для вычисления процентов и значений в промилле
  • Показатели степени в выражениях вроде (f(x) ** 2 + f(y) ** 2) ** 0.5 для √(f(x)² + f(y)²)
  • Литералы 1 и 0 иногда используют для представления булевых значений true и false. Возможно, присваивание этих значений именам вроде TRUE и FALSE было бы лучше.
  • В C и C++ 0 часто используется для обозначения нулевого указателя, хотя стандартная библиотека C определяет макрос NULL, а современный C++ включает ключевое слово nullptr.

Пример

В методе ниже числа 1.2, 30 и 0.015 кодируют бизнес-правила — НДС, льготный период и пеню, — и читателю приходится угадывать, какое из них что означает. Хуже того, 30 встречается дважды и в двух разных смыслах.

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;
    }
}

Теперь у каждого значения есть имя, а льготный и расчётный периоды можно менять независимо друг от друга (см. Замена магического числа символьной константой).

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;
    }
}

Адаптировано по статье Википедии “Magic number (programming)” (версия 1355218563), распространяется по лицензии CC BY-SA 4.0; текст переведён и отредактирован, добавлены примеры.