Устранение магических чисел: когда пришло время сказать «нет»?

36

Мы все знаем, что магические числа (жестко запрограммированные значения) могут нанести ущерб вашей программе, особенно когда пришло время изменить часть кода, в которой нет комментариев, но где вы проводите черту?

Например, если у вас есть функция, которая рассчитывает количество секунд между двумя днями, заменить

seconds = num_days * 24 * 60 * 60

с

seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

В какой момент вы решаете, что совершенно очевидно, что означает жестко закодированное значение, и оставляете его в покое?

oosterwal
источник
2
Почему бы не заменить это вычисление на функцию или макрос, чтобы ваш код в конечном итоге выглядел какseconds = CALC_SECONDS(num_days);
FrustratedWithFormsDesigner
15
TimeSpan.FromDays(numDays).Seconds;
Никто не
18
@oosterwal: С таким отношением ( HOURS_PER_DAY will never need to be altered) вы никогда не будете кодировать программное обеспечение, развернутое на Марсе. : P
FrustratedWithFormsDesigner
23
Я бы сократил количество констант до SECONDS_PER_DAY = 86400. Зачем рассчитывать то, что не изменится?
JohnFx
17
Как насчет високосных секунд?
Джон

Ответы:

40

Есть две причины использовать символические константы вместо числовых литералов:

  1. Для упрощения обслуживания, если магические числа меняются. Это не относится к вашему примеру. Крайне маловероятно, что число секунд в часе или количество часов в дне изменится.

  2. Для улучшения читаемости. Выражение «24 * 60 * 60» довольно очевидно почти для всех. «SECONDS_PER_DAY» тоже, но если вы ищете ошибку, вам, возможно, придется проверить, что SECONDS_PER_DAY был определен правильно. В краткости есть ценность.

Для магических чисел, которые появляются ровно один раз и не зависят от остальной части программы, решение о создании символа для этого числа является делом вкуса. Если есть сомнения, создайте символ.

Не делайте этого:

public static final int THREE = 3;
Кевин Клайн
источник
3
+1 @kevin cline: Я согласен с твоим мнением о краткости охоты на насекомых. Дополнительное преимущество, которое я вижу в использовании именованной константы, особенно при отладке, состоит в том, что если обнаруживается, что константа была определена неправильно, вам нужно всего лишь изменить один фрагмент кода, а не искать в целом проекте все вхождения неправильно реализованного ценность.
oosterwal
40
Или еще хуже:publid final int FOUR = 3;
Габлин
3
О, дорогой, ты, должно быть, работал с тем же парнем, с которым я когда-то работал.
quick_now
2
@ gablin: честно говоря, для пабов весьма полезно иметь крышки.
Алан Пирс
10
Я видел это: public static int THREE = 3;... примечание - нет final!
Стивен С
29

Я бы придерживался правила никогда не иметь магических чисел.

В то время как

seconds = num_days * 24 * 60 * 60

Прекрасно читается большую часть времени после того, как кодировался по 10 часов в день в течение трех или четырех недель в режиме хруста

seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

намного легче читать.

Предложение FrustratedWithFormsDesigner лучше:

seconds = num_days * DAYS_TO_SECOND_FACTOR

или даже лучше

seconds = CONVERT_DAYS_TO_SECONDS(num_days)

Вещи перестают быть очевидными, когда ты очень устал. Код в обороне .

Vitor Py
источник
13
Попадание в режим хруста, как вы описываете, является контрпродуктивным антипаттерном, которого следует избегать. Программисты достигают пика устойчивой производительности около 35-40 часов в неделю.
btilly
4
@ Впрочем, я искренне согласен с тобой. Но это случается, часто из-за внешних факторов.
Vitor Py
3
Я обычно определяю константы для секунды, минуты, дня и часа. Если больше ничего «30 * МИНУТ» действительно легко прочитать, и я знаю, что настало время, не думая об этом.
Захари К
9
@btilly: Пик составляет 35-40 часов или уровень алкоголя в крови между 0,129% и 0,138%. Я прочитал это на XKCD , так что это должно быть правдой!
oosterwal
1
Если бы я увидел константу типа HOURS_PER_DAY, я бы удалил ее, а затем публично унизил вас перед вашими сверстниками. Хорошо, может быть, я бы отказался от публичного унижения, но я бы, наверное, удалил его.
Ed S.
8

Время сказать нет почти всегда. Временами, когда я нахожу, что проще просто использовать жестко запрограммированные числа в таких местах, как макет пользовательского интерфейса - создание константы для позиционирования каждого элемента управления в форме становится очень трудоемким и утомительным, и если этот код обычно обрабатывается дизайнером пользовательского интерфейса, это не имеет большого значения. ... если пользовательский интерфейс не выложен динамически, не использует относительные позиции для некоторой привязки или написан от руки. В этом случае я бы сказал, что лучше определить некоторые значимые константы для макета. И если вам нужен фактор выдумки здесь или там, чтобы выровнять / расположить что-то «как раз правильно», это также должно быть определено.

Но в вашем примере я думаю, что лучше заменить 24 * 60 * 60на DAYS_TO_SECONDS_FACTOR.


Я допускаю, что жестко запрограммированные значения также хороши, когда контекст и использование полностью понятны. Это, однако, является призывом к суждению ...

Пример:

Как указывал @rmx, использование 0 или 1 для проверки, является ли список пустым или, возможно, в границах цикла, является примером случая, когда цель константы очень ясна.

FrustratedWithFormsDesigner
источник
2
Обычно это нормально, 0или 1я считаю. if(someList.Count != 0) ...лучше чем if(someList.Count != MinListCount) .... Не всегда, но в целом.
Никто не
2
@Dima: VS дизайнер форм обрабатывает все это. Если он хочет создать константы, это нормально для меня. Но я не собираюсь вдаваться в сгенерированный код и заменять все жестко закодированные значения константами.
FrustratedWithFormsDesigner
4
Давайте не будем путать код, предназначенный для генерации и обработки инструментом с кодом, написанным для использования человеком.
Бизиклоп
1
@FrustratedWithFormsDesigner, как указал @biziclop, сгенерированный код - это совершенно другое животное. Именованные константы обязательно должны использоваться в коде, который читают и изменяют люди. Сгенерированный код, по крайней мере, в идеальном случае, вообще не должен изменяться.
Дима
2
@FrustratedWithFormsDesigner: Что произойдет, если у вас есть хорошо известное значение, жестко запрограммированное в десятках файлов в вашей программе, которое внезапно необходимо изменить? Например, вы жестко кодируете значение, представляющее количество тактов на микросекунду для встроенного процессора, затем получаете указание перенести ваше программное обеспечение в проект, в котором имеется различное количество тактов на микросекунду. Если ваше значение было чем-то распространенным, например, 8, выполнение поиска / замены для десятков файлов может в конечном итоге вызвать больше проблем.
oosterwal
8

Остановитесь, когда вы не можете закрепить значение или цель числа.

seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

намного легче читать, чем просто используя цифры. (Хотя это можно сделать более читабельным, если иметь одну SECONDS_PER_DAYконстанту, но это совершенно отдельный вопрос.)

Предположим, что разработчик, глядя на код, может видеть, что он делает. Но не думайте, что они тоже знают почему. Если ваш постоянный помогает понять почему, пойти на это. Если нет, не надо.

Если у вас будет слишком много констант, как было предложено в одном ответе, рассмотрите возможность использования внешнего файла конфигурации вместо этого, поскольку наличие десятков констант в файле не совсем улучшает читаемость.

biziclop
источник
7

Я бы, вероятно, сказал «нет» на такие вещи, как:

#define HTML_END_TAG "</html>"

И определенно сказал бы «нет»:

#define QUADRATIC_DISCRIMINANT_COEF 4
#define QUADRATIC_DENOMINATOR_COEF  2
dan04
источник
7

Один из лучших примеров, которые я нашел для продвижения использования констант для очевидных вещей, таких как HOURS_PER_DAY:

Мы подсчитывали, как долго вещи сидят в очереди на работу. Требования были слабо определены, и программист жестко запрограммирован 24во многих местах. В конце концов мы поняли, что было бы несправедливо наказывать пользователей за то, что они сидели за проблемой в течение 24 часов, тогда как на самом деле они работают только по 8 часов в день. Когда задача пришла, чтобы исправить это И посмотреть, какие другие проблемы могут возникать в других отчетах, было довольно трудно выполнить поиск / поиск по коду для 24, было бы намного проще выполнить поиск / поиск по запросу.HOURS_PER_DAY

bkulyk
источник
о нет, поэтому часы в день меняются в зависимости от того, ссылаетесь ли вы на рабочие часы в день (я работаю 7,5 BTW) или на часы в день. Чтобы изменить значение этой константы, вам нужно заменить ее имя на другое. Ваша точка зрения о поиске стало проще, однако.
gbjbaanb
2
Похоже, что в этом случае HOURS_PER_DAY тоже не была желаемой константой. Но возможность искать его по имени - огромное преимущество, даже если (или особенно если) вам нужно изменить его на что-то другое во многих местах.
Дэвид К
4

Я думаю, что до тех пор, пока число является полностью постоянным и не имеет возможности измениться, это вполне приемлемо. Так что в вашем случае seconds = num_days * 24 * 60 * 60это нормально (при условии, конечно, что вы не делаете глупостей, например, выполняете подобные вычисления внутри цикла) и, возможно, лучше для удобства чтения, чем seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE.

Это когда ты делаешь такие вещи, это плохо

lineOffset += 24; // 24 lines to a page

Даже если вы не можете разместить больше строк на странице или даже если у вас нет намерений изменять ее, используйте вместо этого постоянную переменную, потому что однажды она снова станет преследовать вас. В конечном счете, смысл в удобочитаемости, а не в сохранении двух циклов вычислений на процессоре. Это больше не 1978, когда драгоценные байты были сжаты для всей их ценности.

Нил
источник
2
Считаете ли вы приемлемым жестко кодировать неизменное значение 86400 вместо использования именованной константы SECONDS_PER_DAY? Как бы вы убедились, что все вхождения значения были правильными и не пропускали, например, 0 или меняли 6 на 4?
oosterwal
Тогда почему бы не: секунды = число_дней * 86400? Это тоже не изменится.
JeffO
2
Я сделал SECONDS_PER_DAY в обоих направлениях - используя имя и номер. Когда вы вернетесь к коду через 2 года, названный номер ВСЕГДА имеет больше смысла.
quick_now
2
seconds = num_days * 86400 мне не понятно. Вот что в итоге имеет значение. Если я увижу «seconds = num_days * 24 * 60 * 60», кроме того факта, что имена переменных придают значение в этом случае достаточно хорошо, я бы сразу спросил себя, почему я разделил их, и значение становится очевидным, потому что я оставил они как числа (следовательно, они постоянные), а не переменные, к которым дальнейшее исследование потребовало бы, чтобы я понял их значения, и если они постоянны.
Нил
1
Чего люди часто не понимают: если вы измените это значение lineOffset с 24 на 25, вам придется просмотреть весь код, чтобы увидеть, где использовалось 24 и нужно ли его менять, а затем вычислять все дни и часы. умножение на 24 действительно мешает вам.
gnasher729
3
seconds = num_days * 24 * 60 * 60

Это прекрасно. Это на самом деле не магические числа, поскольку они никогда не изменятся.

Любые числа, которые могут разумно измениться или не имеют очевидного значения, должны быть помещены в переменные Что означает почти все из них.

Карра
источник
2
Все seconds = num_days * 86400еще будет приемлемым? Если бы подобное значение использовалось несколько раз в разных файлах, как бы вы убедились, что кто-то случайно не набрал seconds = num_days * 84600в одном или двух местах?
oosterwal
1
Письмо 86400 сильно отличается от письма 24 * 60 * 60.
Carra
4
Конечно это изменится. Не каждый день имеет 86 400 секунд. Рассмотрим, например, летнее время. Раз в году, в некоторых местах есть только 23 часов в день, а в другой день они будут иметь 25. пуф ваши номера разбиты.
Дейв Делонг,
1
@ Дэйв, отлично. Существуют дополнительные секунды - en.wikipedia.org/wiki/Leap_second
Честная оценка. Добавление функции будет гарантией, если вам когда-нибудь понадобится перехватить эти исключения.
Карра
3

Я бы не стал создавать константы (магические значения) для преобразования значения из одной единицы в другую. В случае конвертации я предпочитаю имя говорящего метода. В этом примере это будет, например, DayToSeconds(num_days)внутренний метод не нуждается в магических значениях, потому что значение «24» и «60» понятно.

В этом случае я бы никогда не использовал секунды / минуты / часы. Я бы использовал только TimeSpan / DateTime.

Kays
источник
1

Используйте контекст в качестве параметра, чтобы решить

Например, у вас есть функция, которая называется «CalculateSecondsBetween: aDay and: anotherDay», вам не нужно много объяснять, что делают эти числа, потому что имя функции довольно представительное.

И еще один вопрос: какие есть возможности рассчитать его по-другому? Иногда есть много способов сделать то же самое, поэтому, чтобы помочь будущим программистам и показать им, какой метод вы использовали, определение констант может помочь понять это.

guiman
источник