消除魔术数字:什么时候该说“不”?


36

我们都知道,幻数(硬编码值)会对您的程序造成严重破坏,尤其是当需要修改没有注释的一段代码时,您会在哪里划清界线?

例如,如果您有一个计算两天之间的秒数的函数,您是否要替换

seconds = num_days * 24 * 60 * 60

seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

您在什么时候确定硬编码值意味着什么是完全显而易见的,而不管它呢?


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 2011年

17
leap秒呢?
约翰,

Answers:


40

使用符号常量而不是数字文字有两个原因:

  1. 为了简化维护,如果幻数发生变化。这不适用于您的示例。一小时的秒数或一天中的小时数极不可能改变。

  2. 为了提高可读性。几乎每个人都很容易理解“ 24 * 60 * 60”这个表达。“ SECONDS_PER_DAY”也是如此,但是如果您正在寻找错误,则可能必须检查SECONDS_PER_DAY是否已正确定义。简洁有价值。

对于仅出现一次且独立于程序其余部分的魔术数字,决定是否为该数字创建符号是一个品味问题。如有任何疑问,请继续创建符号。

不要这样做:

public static final int THREE = 3;

3
+1 @kevin cline:我同意您的观点,即简洁性是一个错误寻找。我看到使用命名常量(尤其是在调试时)的附加好处是,如果发现常量定义不正确,则只需更改一段代码,而无需在整个项目中搜索所有错误实现的实例。值。
oosterwal 2011年

40
甚至更糟:publid final int FOUR = 3;
gablin 2011年

3
哦,亲爱的,您一定和我曾经一起工作过的那个人一起工作过。
quick_now 2011-03-10

2
@gablin:公平地说,酒吧有盖很有用。
艾伦·皮尔斯

10
我已经看到了:public static int THREE = 3;...注意-不final
斯蒂芬·C

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)

当你很累的时候,事情就会停止。防御性地编码


13
进入您描述的紧缩模式是适得其反的反模式,应避免。程序员大约每周35-40小时达到最高的持续生产力。
btilly 2011年

4
@btilly我完全同意你的看法。但它的发生通常是由于外部因素造成的。
Vitor Py

3
我通常为秒,分钟,天和小时定义常量。如果说“ 30 * MINUTE”真的很容易阅读,我知道它的时间就不用考虑了。
Zachary K

9
@btilly:高峰是35-40小时,或者血液中的酒精含量在0.129%和0.138%之间。我在XKCD上阅读过它,所以它必须是真实的!
oosterwal 2011年

1
如果我看到一个像HOURS_PER_DAY这样的常量,我将其删除,然后在同伴面前公开羞辱您。好的,也许我会放弃公众的屈辱,但我可能会删除它。
Ed S.

8

拒绝的时间几乎总是如此。我发现使用硬编码数字更容易的时候是在UI布局之类的地方-为表单上每个控件的定位创建一个常量会非常麻烦和累人,并且如果该代码通常由UI设计师处理,没什么大不了的。...除非用户界面是动态布置的,或者使用相对于某个锚点的相对位置,或者是手工编写的。在那种情况下,我想最好为布局定义一些有意义的常量。而且,如果您在此处或那里需要一个软糖因素来对齐/定位“恰好”的东西,则也应该定义它。

但是,在你的榜样,我认为,更换24 * 60 * 60DAYS_TO_SECONDS_FACTOR是更好的。


我承认,当上下文和用法完全清楚时,硬编码值也可以。但是,这是一个判断电话。

例:

正如@rmx所指出的,使用0或1来检查列表是否为空,或者是否处于循环范围内是一个例子,该例子的目的非常明确。


2
它通常是确定以使用01我看。if(someList.Count != 0) ...比更好if(someList.Count != MinListCount) ...。并非总是如此,但通常如此。
没人

2
@Dima:VS表单设计者可以处理所有这些。如果它想创建常量,那对我很好。但是我打算讨论生成的代码,也不用常量替换所有硬编码的值。
FrustratedWithFormsDesigner

4
但是,不要将要由工具生成和处理的代码与为人类使用而编写的代码混淆。
biziclop 2011年

1
@biziclop指出@FrustratedWithFormsDesigner,生成的代码是完全不同的动物。绝对必须在人们读取和修改的代码中使用命名常量。至少在理想情况下,完全不应修改生成的代码。
迪马

2
@FrustratedWithFormsDesigner:当您在程序中的数十个文件中硬编码了一个众所周知的值而突然需要更改时,会发生什么?例如,您对表示嵌入式处理器每微秒时钟滴答数的值进行了硬编码,然后被告知将软件移植到每微秒时钟滴答数不同的设计。如果您的价值很普遍(例如8),那么对数十个文件执行查找/替换可能会导致更多问题。
oosterwal

8

当您无法确定数字的含义或目的时,请停止。

seconds = num_days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE

比仅使用数字更容易阅读。(尽管可以通过单个SECONDS_PER_DAY常量使它更具可读性,但这是一个完全独立的问题。)

假定查看代码的开发人员可以看到它的作用。但是不要以为他们也知道为什么。如果您的常数有助于理解原因,那就去吧。如果没有,那就不要。

如果最终得到的常量过多(如一个答案所建议),请考虑使用外部配置文件,因为在文件中包含数十个常量并不能完全提高可读性。


7

我可能会对诸如此类的事情说“不”:

#define HTML_END_TAG "</html>"

而且肯定会说“不”:

#define QUADRATIC_DISCRIMINANT_COEF 4
#define QUADRATIC_DENOMINATOR_COEF  2

7

我发现的促进显而易见的事物使用常量的最好的例子之一HOURS_PER_DAY是:

我们正在计算事物在一个人的工作队列中待了多长时间。需求定义松散,程序员24在许多地方进行了硬编码。最终,我们意识到惩罚惩罚用户每天24小时就只能坐在一个问题上,而这实际上一天只能工作8个小时。当任务来解决此问题并查看哪些其他报告可能存在相同的问题时,grep /搜索24个代码非常困难,而grep / search则要容易得多。HOURS_PER_DAY


哦,是的,所以每天的小时数会根据您是指每天的工作时间(我的工作时间为7.5 BTW)还是一天中的小时数而有所不同。要像这样更改常量的含义,您需要将其名称替换为其他名称。不过,您关于轻松搜索的观点是正确的。
gbjbaanb 2012年

2
在这种情况下,似乎HOURS_PER_DAY也不是所需的常数。但是,能够根据名称搜索是一个巨大的好处,即使(或特别是如果)你需要将其更改为别的东西在很多地方。
David K

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

即使您再也无法容纳页面上的行,或者即使您无意更改它,也请改用常量变量,因为有一天它会回来困扰您。最终,重点是可读性,而不是在CPU上节省2个计算周期。到了1978年,所有宝贵的字节都被压缩了。


2
您会发现硬编码不变值86400而不使用命名常量SECONDS_PER_DAY是否可以接受?例如,您将如何验证所有出现的值都是正确的并且没有丢失0或交换6 ad 4?
oosterwal 2011年

那为什么不呢:秒= num_days * 86400?那也不会改变。
JeffO 2011年

2
我已经完成SECONDS_PER_DAY的两种操作-使用名称和使用数字。两年后,当您再次使用该代码时,命名数字总是更有意义。
quick_now 2011-03-10

2
秒= 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 2011年

1
写作86400是非常不同的书写24 * 60 * 60
卡拉格

4
当然会改变。并非每天都有86,400秒。考虑例如夏时制。每年一次,有的地方只有有23个小时了一天,又一天,他们将有25 您的号码被打破。
Dave DeLong

1
@戴夫,好点子。闰秒存在- en.wikipedia.org/wiki/Leap_second

有道理。如果您需要捕获那些异常,添加功能将是一种安全措施。
卡拉

3

我会避免创建常量(魔术值)以将值从一个单位转换为另一个单位。在进行转换的情况下,我更喜欢口语方法名称。在此示例中,这例如是DayToSeconds(num_days)内部方法不需要魔术值,因为“ 24”和“ 60”的含义很清楚。

在这种情况下,我永远不会使用秒/分钟/小时。我只会使用TimeSpan / DateTime。


1

使用上下文作为参数来决定

例如,您有一个名为“ calculateSecondsBetween:aDay和:anotherDay”的函数,由于这些函数名具有代表性,因此无需对这些数字的作用做太多解释。

另一个问题是,以不同的方式进行计算的可能性有哪些?有时有很多方法可以做同样的事情,因此可以指导未来的程序员并向他们展示您使用了哪种方法,定义常量可以帮助您弄清这一点。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.