什么是魔法数字,为什么有些人认为它们不好?

615
什么是魔数?
为什么许多程序员建议避免使用它们?

6
您最好避免使用“魔数”,因为其他看您代码的人可能不明白您为什么要这样做。例如,const myNum = 22; const number = myNum / 11; 现在我的11可能是人或啤酒瓶之类的东西,因此我会将其更改为常量,例如inhabitants。 - user6913790
在属性中使用魔法数字是不可避免的,所以我想这是合适的。 - donatasj87
15个回答

701
一个魔数是指在代码中直接使用的数字。
例如,在Java中,如果您有以下代码:
public class Foo {
    public void setPassword(String password) {
         // don't do this
         if (password.length() > 7) {
              throw new InvalidArgumentException("password");
         }
    }
}

应该进行重构:

public class Foo {
    public static final int MAX_PASSWORD_SIZE = 7;

    public void setPassword(String password) {
         if (password.length() > MAX_PASSWORD_SIZE) {
              throw new InvalidArgumentException("password");
         }
    }
}

它可以提高代码的可读性,并且更易于维护。想象一下,当我在GUI中设置密码字段的大小时,如果我使用一个神奇数字,每当最大大小更改时,我必须在两个代码位置上进行更改。如果我忘记了其中一个,这将导致不一致性。
JDK中充满了像Integer,Character和Math类中那样的示例。
PS:静态分析工具(如FindBugs和PMD)会检测到您代码中使用的神奇数字,并建议进行重构。

209
这条规则的例外是0和1。 - Jonathan Parker
48
@Jonathan Parker,除非他们不是(TRUE/FALSE)。 - Brendan Long
110
虽然一个魔数永远不会改变,但它也应该用常量替换。我的代码中充满了全局常量,如HzPerMHz和msecPerSecond。它们永远不会改变,但它们可以使含义更清晰,并提供一些防止打错字的保护。 - Jeanne Pindar
16
即使在看似微不足道的方法中,也要养成定义常量以赋予那些原本“任意”的数字以含义的习惯。为什么呢?因为将来可能会将该方法添加到其他代码中,而最初显然的内容现在可能已经隐藏在许多代码行之中。至于担心代码混乱,据我所知,所有体面的现代IDE都能轻松找到常量变量的值,通常只需将鼠标指针悬停在变量的任何使用上即可。即使在过去我们没有这样的便利时,从长远来看,这也是非常非常值得的。 - ToolmakerSteve
9
@Sergey: "比如这样的东西:connection.setTimeout (50); 这里的 50 应该是一个常量吗?很明显,50 是连接超时时间。" - 无论是单次还是多次调用,这都是一个神奇数字。为什么是50?为什么不是51?你可能需要更改它,因为你正在部署到需要不同超时时间的环境。我宁愿更改一个常量,也不愿意在代码中寻找。 - user585968
显示剩余16条评论

179
一个魔术数字是一个硬编码的值,它可能在以后的阶段更改,但因此很难更新。例如,假设您有一个页面,显示“您的订单”概览页面中的最后50个订单。这里的50就是魔术数字,因为它不是通过标准或约定设置的,而是出于规范中概述的原因而由您创造的数字。现在,您需要在不同的位置使用50 - 您的SQL脚本(SELECT TOP 50 * FROM orders),您的网站(您的最后50个订单),您的订单登录(for (i = 0; i < 50; i++))和可能许多其他地方。现在,当有人决定将50更改为25、75或153时会发生什么?您现在必须在所有位置替换50,而且很可能忽略掉某些地方。查找/替换可能行不通,因为50可能用于其他用途,并且盲目地用25替换50可能会产生一些其他不良副作用(即Session.Timeout = 50调用,也设置为25并且用户开始报告超时太频繁)。此外,代码可能很难理解,例如“if a < 50 then bla” - 如果你在复杂的函数中遇到这种情况,其他不熟悉代码的开发人员可能会问自己“50是什么意思?”。这就是为什么最好将这些模糊和任意数字放在完全1个地方 - “const int NumOrdersToDisplay = 50”,因为这使得代码更易读(“if a < NumOrdersToDisplay”),这也意味着您只需要在1个明确定义的位置进行更改。魔术数字适用于所有通过标准定义的内容,例如SmtpClient.DefaultPort = 25或TCPPacketSize = whatever(不确定是否标准化)。同样,仅在一个函数中定义的所有内容可能是可以接受的,但这取决于上下文。

28
即使它不能改变,仍然是一个糟糕的想法,因为不清楚发生了什么。 - Loren Pechtel
21
有时候并不是不清楚。SmtpClient.DefaultPort = 25 可能比 SmtpClient.DefaultPort = DEFAULT_SMTP_PORT 更加清晰明了。 - user253751
7
假设没有其他代码使用 DEFAULT_SMTP_PORT 这个概念。如果该应用程序的默认SMTP端口更改,则需要在多个位置进行更新,可能会导致不一致性。 - Russ Bradberry
5
找到所有使用情况也更困难 - 你需要在整个应用程序中搜索“25”,并确保只更改用于SMTP端口的“25”出现,而不是例如表列的宽度或要显示在页面上的记录数的“25”。 - Michael Stum
4
在那个例子中,我期望代码使用 SmtpClient.DefaultPort 而不是 25。因此,您只需要在一个地方进行更改。端口号很可能保持不变,它不是一个随机的神奇数字,而是由 IANA 分配的数字。 - njsg

43

你看过魔数的维基百科页面了吗?

该页面详细介绍了魔数引用的所有方式。以下是关于魔数作为不良编程实践的一段引用:

术语“魔数”也指在源代码中直接使用数字而不加说明的不良编程实践。在大多数情况下,这会使程序更难阅读、理解和维护。虽然大多数指南对零和一这两个数字有例外,但最好将代码中的所有其他数字定义为命名常量。


36

魔数(Magic Number)与符号常量(Symbolic Constant):何时应该替换?

魔数:未知的语义

符号常量 -> 提供正确的语义和正确的使用上下文

语义:事物的含义或目的。

"创建一个常量,将其命名为其含义,并将数字替换为它。" -- Martin Fowler

首先,魔数不仅仅是数字。任何基本值都可以是“魔数”。基本值是明显的实体,例如整数、实数、双精度浮点数、浮点数、日期、字符串、布尔值、字符等等。问题不在于数据类型,而在于“魔力”值在代码文本中出现的方面。

我们所说的“魔数”是什么意思呢?准确地说:“魔数”是指我们的代码上下文中值的语义(含义或目的);它是未知的、无法知晓的、不清楚的或混淆的。这就是“魔数”的概念。当基本值的语义含义或存在目的从周围上下文中很快、很容易地得知、清楚并理解(不混淆),而不需要特殊的帮助词(例如符号常量)时,则不是“魔数”。

因此,我们通过测量代码读者从上下文中知道、清楚和理解基本值的含义和目的的能力来确定魔数。读者越不知道、不清楚和困惑,基本值就越“神奇”。

基础知识

我们有两种情况下出现基本值的魔力。只有第二种情况对程序员和代码来说才是最重要的:

  1. 孤立的基本值(例如数字),其含义未知、无法知晓、不清楚或混淆。
  2. 上下文中的基本值(例如数字),但其含义仍然未知、无法知晓、不清楚或混淆。

“魔力”的一个重要前提是,孤立的基本值(例如数字)没有常见的语义(如圆周率),但具有局部已知的语义(例如你的程序),这在上下文中并不完全清晰或可能被滥用于良好或恶劣的上下文中。

大多数编程语言的语义不允许我们使用孤立的基本值,除非(也许)作为数据(即数据表)。当我们遇到“魔数”时,通常是在上下文中。因此,对于

"我要用符号常量替换这个魔数吗?"

答案是:

"您能多快地评估和理解数字的语义含义(它在上下文中的目的)?"

有点神奇,但不完全

考虑到这一点,我们可以很快看到像圆周率(3.14159)这样的数字在适当的上下文中并非是一个“魔数”(例如 2 x 3.14159 x 半径或 2πr)。在这里,数字3.14159心理上被认为是圆周率,而没有符号常量标识。

尽管如此,由于Pi的长度和复杂性,我们通常会用符号常量标识代替3.14159。由于Pi的长度和复杂性(以及需要精度),符号常量标识较少出错。把“π”作为名称进行识别只是一个方便的奖励,但不是具有该常量的主要原因。

同时,在农场里

暂且抛开像圆周率这样的常量,让我们主要关注那些具有特殊含义的数字,但其含义仅限于我们软件系统的宇宙之内。这样的数字可能是“2”(作为基本整数值)。

如果我仅使用数字2,则我的第一个问题可能是:“‘2’是什么意思?”仅仅使用数字“2”而没有上下文环境,其意义未知,无法得知其用途。尽管由于语言语义,我们的软件中不会仅使用“2”,但我们仍然希望看到,单独的数字“2”没有特殊的语义或明显的目的。

让我们将孤立的“2”放入这样的上下文中:padding := 2,其中上下文是“GUI容器”。在这种情况下,“2”的含义(以像素或其他图形单位)可以让我们快速猜测其语义(含义和目的)。我们可以在此停止,并说“2”在这个上下文中很好,我们无需了解其他信息。然而,也许在我们的软件宇宙中,这还不是全部。它还有更多的含义,但是“padding = 2”作为上下文无法揭示这一点。

让我们进一步假设,程序中作为像素填充的“2”是整个系统中的“default_padding”类型。因此,仅写指令padding = 2是不够的。我们并没有透露“default”的概念。只有当我在上下文中写出: padding = default_padding,然后在其他地方写出: default_padding=2时,我才能充分认识到“2”在我们的系统中的更好和更全面的含义(语义和目的)。

上面的例子很好,因为“2”本身可以是任何东西。只有当我们将理解的范围和域限制为“我的程序”时,“2”才可以在其适当的上下文中得到理解,它是“我的程序”GUI UX部分中的"default_padding"。在此处,“2”是一个“魔法数字”,它被分解为符号常量"default_padding",以使它在封闭代码的更大上下文中快速理解。
因此,任何基本值,其含义(语义和目的)无法快速和充分地理解的都可以作为符号常量的良好候选者,以取代基本值(例如“魔法数字”)。
进一步说,比例尺上的数字也可能具有语义。例如,假设我们正在制作一个D&amp;D游戏,其中有一个怪物概念。我们的怪物对象有一个称为“life_force”的特征,它是一个整数。这些数字没有足够的含义和清晰的意义,除非有单词来提供含义。因此,我们首先任意地说:
full_life_force:INTEGER = 10 -- 非常活着(没有受伤) minimum_life_force:INTEGER = 1 -- 勉强活着(非常受伤) dead:INTEGER = 0 -- 死了 undead:INTEGER = -1 -- 最小的亡灵(快要死了) zombie:INTEGER = -10 -- 最大的亡灵(非常亡灵)
从上面的符号常量中,我们开始对我们D&amp;D游戏中的怪物的活力,死亡和“亡灵”(以及可能的后果或后果)形成一个心理图像。如果没有这些单词(符号常量),我们只有范围为-10..10 的数字。仅有范围而没有单词会让我们处于可能极度混乱的位置,并且在不同的游戏部分具有依赖关系时可能会导致游戏错误,例如attack_elvesseek_magic_healing_potion等操作。
因此,在寻找和考虑替换“魔法数字”时,我们想要针对我们软件的上下文以及数字如何在语义上相互交互的非常目的化的问题提问。
结论是,我们应该问以下问题: - 当...时,您可能有一个魔术数字...
  1. 在您的软件世界中,基本值是否可以具有特殊含义或目的?
  2. 即使在适当的上下文中,这种特殊含义或目的可能会是未知的、无法知晓的、不清楚的或令人困惑的吗?
  3. 在错误的上下文中,可以使用正确的基本值造成糟糕后果吗?
  4. 在正确的上下文中,可以使用不正确的基本值造成糟糕后果吗?
  5. 在特定上下文中,基本值是否与其他基本值具有语义或目的关系?
  6. 一个基本值是否可以存在于代码中的多个位置,并在每个位置具有不同的语义,从而导致读者混淆?

检查代码文本中独立的显式常量基本值。针对每个此类值实例,缓慢、深思熟虑地询问每个问题。考虑您的回答的强度。许多时候,答案并不是非黑即白的,而是存在着对含义和目的的误解、学习速度和理解速度的程度差异。同时还需要看它如何与周围的软件机器相连接。

最终,替换的答案是回答读者连接(例如“理解”)的强度或弱点的度量(在您的头脑中)。他们理解含义和目的的速度越快,你就会有更少的“魔法”。

结论:只有当“魔法”足够大以至于会因混淆而导致难以检测的错误时,才使用符号常量来替换基本值。


3
谢谢。FWIW,我的同事们一直在安装静态分析工具,这些工具不断抱怨“神奇数字”——但是工具该如何理解语义呢?结果是所有基本值都被替换为符号常量。尽管我同意你的结论,但我认为这并不是最理想的做法。 - Chomeh
2
应该使用符号“PI”而不是“3.14159”的两个原因:1. 它可以防止打字错误。如果常量拼写错误,编译器会捕获,但如果数字错漏,编译器将无法检测到。在一个地方放置数字更少出错。最理想的情况是从许多项目使用的数学库中获取该数字。2. 确保你的整个应用程序使用相同数量的具有足够有效数字的PI值。在某些位置使用“3.14”而在其他位置使用“3.14159”可能会导致错误。理想情况下,您的定义将具有由数据类型允许的最大有效数字。 - Stephen Ostermiller

22

魔数是文件格式或协议交换开头的一系列字符。这个数字用作检查其合理性。

例如:打开任何GIF文件,你会在开头看到GIF89。其中"GIF89"就是魔数。

其他程序可以读取文件的前几个字符并正确识别GIF。

危险在于,随机的二进制数据可能包含相同的字符。但这种情况非常不太可能发生。

至于协议交换,你可以使用它快速识别当前传递给你的'消息'是否已损坏或无效。

魔数仍然很有用。


19
我不认为那是他所指的“魔法数字”。 - Marcio Aguiar
5
也许你应该删除你添加的“文件格式”和“网络”标签,因为他显然不是在谈论那些类型的魔数。 - Landon
12
了解魔数可能不仅仅是代码问题仍然非常有用。-Adam - Adam Davis
3
如果主题为:"源代码中的魔数是什么",则标签不应该存在。但他没有明确指定。因此提供额外信息很有用。我认为Kyle、Landon和Marcio是错误的。 - Brian R. Bondy
4
他也无法确定他在找哪一个。由于我是第一个发帖的,所以我无法猜测他在找哪个。 - Brian R. Bondy

20
在编程中,“魔术数字”是一个应该被赋予符号名称的值,但实际上却被直接写入代码中,通常在多个地方出现。
这样做的原因与SPOT(单一真相)相反,因为如果您想以后更改这个常量,您必须搜索整个代码来查找每个实例。此外,它也不好,因为其他程序员可能不清楚这个数字代表什么,因此出现“魔术数字”的情况。
有时人们会进一步消除魔术数字,通过将这些常量移动到单独的文件中作为配置文件。这样做有时很有帮助,但有时会造成比它本身价值更高的复杂性。

你能更具体地说明为什么消除魔数并不总是好的吗? - Marcio Aguiar
5
Marcio:当你像“const int EIGHT = 8;”这样做的时候,如果需求发生变化,结果可能会变成“const int EIGHT = 9;”。 - jmucchiello
10
抱歉,但这只是一个糟糕命名的例子,或者该常量的一种基本用法。 - Kzqai
3
在某些平台上,像(foo[i]+foo[i+1]+foo[i+2]+1)/3这样的表达式可能比循环计算要快得多。如果将3替换为其他数字(例如5),而不重写代码为循环,那么看到ITEMS_TO_AVERAGE被定义为3的人可能会认为可以将其改为5以便平均更多的项。相比之下,看到具有字面意义的数字3的表达式的人会意识到3表示正在加在一起的项的数量。 - supercat
2
那么quadratic_root = (-b + sqrt(b*b - 4*a*c)) / (2*a)怎么样?实际上,任何数学公式中,“魔法数字”除了在公式中没有其他意义。 - jodag
显示剩余2条评论

13

使用魔术数字存在一个未被提及的问题...

如果您有很多魔术数字,那么有相当大的几率您会为两个不同的目的使用魔术数字,其恰好相同。

然后,不出所料,您需要更改值...只针对一个目的。


当谈到数字时,这看起来并不太可能(至少对我来说),但我在处理字符串时遇到了这个问题:首先,您必须阅读大量代码以查看它的使用位置,然后您必须注意到它被用于不同的事情...这不是我最喜欢的消遣方式。 - Tomislav Nakic-Alfirevic

12
一个魔数也可以是具有特殊硬编码语义的数字。例如,我曾经看到一个系统,其中记录ID> 0被正常处理,0本身是“新记录”,-1是“这是根”,-99是“在根中创建”。 0和-99会导致WebService提供新的ID。
这种方法的问题在于,您正在重用带符号整数的空间以实现特殊功能。也许您永远不想使用ID为0或负ID创建记录,但即使没有,每个查看代码或数据库的人可能都会首先被困惑。不用说这些特殊值并没有得到很好的记录。
可以说,22、7、-12和620 也算是魔数。;-)

9

我猜这是对我之前问题的答案的回应。在编程中,魔数是一个嵌入的数字常量,没有解释就出现了。如果它在两个不同的位置出现,可能会导致一个实例被更改而另一个实例没有被更改。因此,为了这两个原因,将数字常量与使用它们的地方隔离和定义是很重要的。


5
我一直将“魔数”这个术语用于不同的含义,它是存储在数据结构中的一个模糊值,可以通过快速验证来检查其有效性。例如,gzip文件的前三个字节为0x1f8b08,Java类文件以0xcafebabe开头等。
通常可以在文件格式中嵌入魔数,因为文件可能会被随意发送并丢失有关其创建方式的任何元数据。但是,魔数有时也用于内存数据结构,例如ioctl()调用。
在处理文件或数据结构之前快速检查魔数可以让您及早发现错误,而不是在整个处理过程中遇到无效输入后才进行报告。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接