什么是幻数,为什么不好呢?[关闭]


514

什么是魔数?

为什么要避免呢?

在某些情况下合适吗?


2
您应该避免使用幻数,因为其他人在查看您的代码时可能不理解您为什么要做自己的工作……例如,const myNum = 22; const number = myNum / 11;现在我的11岁可能是人或啤酒瓶之类的东西,所以我将11改为常数例如居民。
JuicY_Burrito

在属性中不可避免地使用幻数,因此我认为这是适当的。
donatasj87 '19

Answers:


574

幻数是代码中数字的直接用法。

例如,如果您拥有(使用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中充满了类似的例子IntegerCharacterMath类。

PS:诸如FindBugs和PMD之类的静态分析工具会检测代码中幻数的使用,并提出重构建议。


174
0和1是该规则的例外。
乔纳森·帕克

24
@Kirill:如果您希望“百分率”的定义发生变化,那么可以。更好的方法是将变量从变量的值表示为变量的值,即,公共静态最终变量MAX_DOWNLOAD_PERCENTAGE =100。尽管那样也没有意义,因为“ 100%”的定义非常明确。另一方面,密码最长不能超过7个字符的事实尚未全局定义,并且实际上并不相同,因此可以作为变量的候选者。
迈克尔·斯托姆

40
@乔纳森·帕克(Jonathan Parker),除非他们不在(TRUE/ FALSE
布伦丹·朗·朗

82
仅仅因为一个魔术数字永远不会改变并不意味着它不应该被一个常数代替。我的代码充满了全局常量,例如HzPerMHz和msecPerSecond。这些永远不会改变,但是它们使含义更清楚,并提供了防止错别字的保护。
Jeanne Pindar 2010年

43
@MarcusJ您再没有错了。这不是意见问题,而是许多程序员的来之不易的经验。我无法告诉您,在过去的40年编程中,我曾咒骂过一位未定义常量的程序员,所以我只发现了直接使用数字的情况,这需要在代码维护期间理解,埋在很多代码中的某个地方,通过定义这样的常量可以清楚其含义。同样,任何其他高级程序员在这方面也会有多个恐怖故事。
ToolmakerSteve

145

幻数是一个硬编码的值,它可能在以后的阶段中更改,但是因此可能很难更新。

例如,假设您有一个页面显示“您的订单”概述页面中的最后50个订单。50是此处的幻数,因为它不是通过标准或约定设置的,它是由规范中概述的原因组成的数字。

现在,您要做的是在不同的位置拥有50个位置-您的SQL脚本(SELECT TOP 50 * FROM orders),您的网站(您的最近50个订单),您的订单登录名(for (i = 0; i < 50; i++))以及其他许多地方。

现在,当有人决定将50更改为25时会发生什么?还是75?还是153?现在您必须在所有位置替换50个,您很可能会错过它。“查找/替换”可能不起作用,因为50可能用于其他用途,而盲目地将50替换为25可能会带来其他一些不良影响(例如,您的Session.Timeout = 50呼叫也设置为25,并且用户开始报告过于频繁的超时)。

另外,代码可能很难理解,即“ if a < 50 then bla”-如果您在复杂的函数中间遇到该代码,其他不熟悉该代码的开发人员可能会问自己“ WTF是50 ???”。

这就是为什么最好在正好1个位置包含这样的模棱两可和任意的数字-“ const int NumOrdersToDisplay = 50”,因为这会使代码更具可读性(“ if a < NumOrdersToDisplay”,这也意味着您只需要在1个明确定义的位置进行更改即可。

魔术数字合适的地方是通过标准定义的所有内容,即SmtpClient.DefaultPort = 25TCPPacketSize = whatever(不确定是否标准化)。同样,仅在1个函数内定义的所有内容都可以接受,但这取决于上下文。


18
即使无法更改,但这仍然不是一个好主意,因为尚不清楚发生了什么。
罗伦·佩希特尔

11
并非总是不清楚。SmtpClient.DefaultPort = 25可能是明显的ERSmtpClient.DefaultPort = DEFAULT_SMTP_PORT
user253751 2014年

4
@immibis我想这是假设绝对没有其他代码使用DEFAULT_SMTP_PORT的概念。如果更改了该应用程序的默认SMTP端口,则需要在多个位置进行更新,从而可能导致不一致。
Russ Bradberry

3
查找所有用法也更加困难-您必须25在整个应用程序中进行搜索,并确保仅更改25SMTP端口的出现,而不是更改25的出现(例如表列的宽度或数字)记录显示在页面上。
Michael Stum

2
在该示例中,我希望代码使用SmtpClient.DefaultPort,而不是25。因此,您只需要在一个位置进行更改即可。而且端口号可能保持不变,这不是随机的幻数,而是由分配的数字IANA
njsg 2015年

34

您是否已查看Wikipedia条目中的魔术数字?

它详细介绍了魔术数字引用的所有创建方式。这是关于魔术数作为一种不好的编程习惯的报价

魔术数字一词还指不正确地直接在源代码中使用数字的不良编程习惯。在大多数情况下,这会使程序更难以阅读,理解和维护。尽管大多数指南都将数字零和一作为例外,但是将代码中的所有其他数字定义为命名常量是个好主意。


8
RTFW的好例子:)
伊娃

我会说答案还远远不够。
Skeeve '18年

25

幻数与 符号常量:何时更换?

魔术:未知语义

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

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

“创建一个常量,在含义后命名,然后用它替换数字。” -马丁·福勒(Martin Fowler)

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

“魔术”是什么意思?确切地说:通过“魔术”,我们打算在代码的上下文中指向值的语义(含义或目的);它是未知的,不可知的,不清楚的或令人困惑的。这就是“魔术”的概念。当基本含义的语义或存在目的在没有特殊辅助词(例如符号常量)的情况下,可以从周围环境中快速,轻松地了解,清楚和理解(不会混淆)时,就不是魔术。

因此,我们通过测量代码阅读器从周围环境中了解,清楚并理解基本值的含义和目的的能力,来确定幻数。读者越不了解,不清楚和混乱,其基本价值就越“神奇”。

有用的定义

  • 混淆:使(某人)感到困惑或困惑。
  • 困惑:使(某人)感到困惑和困惑。
  • 困惑:完全困惑 很困惑。
  • 困惑:完全困惑或困惑。
  • 困惑:无法理解;困惑。
  • 理解:感知(单词,语言或说话者)的预期含义。
  • 含义:单词,文本,概念或动作的含义。
  • 意图:意图传达,指示或提及(特定事物或概念);表示。
  • 表示:表示。
  • 指示:指示某事的标志或信息。
  • 指出:指出 显示。
  • 符号:物体,质量或事件,其存在或出现指示其他事物的可能存在或发生。

基本

我们为魔术基本值提供了两种方案。对于程序员和代码,只有第二个是最重要的:

  1. 一个唯一的基本值(例如数字),其含义未知,未知,不清楚或令人困惑。
  2. 上下文中的基本值(例如,数字),但其含义仍然未知,不可知,不清楚或令人困惑。

“魔术”的总体依赖性是孤单的基本值(例如,数字)如何不具有众所周知的语义(例如Pi),但是具有本地已知的语义(例如,您的程序),这种语义从上下文中不能完全清楚或可能被滥用在好或坏的情况下。

大多数编程语言的语义都不允许我们使用单独的基本值,除了(也许)作为数据(即数据表)之外。当我们遇到“幻数”时,通常是在上下文中。因此,答案

“我用一个符号常数替换这个幻数吗?”

是:

“在上下文中,您能多快地评估和理解数字的语义含义(其存在的目的)?”

有点魔术,但不完全是

有了这个想法,我们可以很快地看到像Pi(3.14159)这样的数字在适当的上下文中(例如2 x 3.14159 x半径或2 * Pi * r)不是“魔术数”。在此,数字3.14159是没有符号常数标识符的心理识别Pi。

尽管如此,由于数字的长度和复杂性,我们通常用符号常数标识符(如Pi)替换3.14159。Pi的长度和复杂性(以及对准确性的要求)方面通常意味着符号标识符或常数不太容易出错。将“ Pi”识别为名称只是一个方便的奖励,但不是拥有常量的主要原因。

同时:回到牧场

除了Pi之类的常见常量外,我们主要关注具有特殊含义的数字,但是这些含义仅限于我们软件系统的范围。这样的数字可能是“ 2”(作为基本整数值)。

如果我自己使用数字2,那么我的第一个问题可能是:“ 2”是什么意思?在没有上下文的情况下,“ 2”本身的含义是未知的和不可知的,从而使其使用不清楚且令人困惑。即使由于语言语义而不会在软件中仅包含“ 2”,我们也确实希望看到“ 2”本身没有任何特殊的语义或明显的目的。

让我们将单独的“ 2”放在以下上下文中:padding := 2,其中上下文是“ GUI容器”。在这种情况下,2的含义(以像素或其他图形单位表示)为我们提供了对其语义(含义和目的)的快速猜测。我们可能会在这里停下来说2在这种情况下还可以,并且我们不需要知道其他任何信息。但是,也许在我们的软件世界中,这还不是全部。还有更多内容,但是“ padding = 2”作为上下文无法显示出来。

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

上面的示例非常好,因为“ 2”本身可以是任何东西。只有当我们将理解的范围和领域限制在“我的程序”中,其中2是“我的程序” default_padding的GUI UX部分中时,才最终在其适当的上下文中理解“ 2”。在这里,“ 2”是一个“魔术”数字,default_padding在“我的程序”的GUI UX上下文中将其分解为符号常量,以便使其default_padding在封闭代码的更大上下文中能够快速理解。

因此,其含义(语义和目的)无法充分,快速地理解的任何基本值都可以代替基本值(例如,幻数)而成为符号常量的良好候选者。

走得更远

规模上的数字也可能具有语义。例如,假设我们正在制作D&D游戏,其中我们有一个怪物的概念。我们的怪物对象具有的功能life_force,它是一个整数。这些数字的含义是无法理解或清楚的,没有文字可以提供含义。因此,我们首先随意说:

  • full_life_force:INTEGER = 10-非常活跃(不受伤害)
  • minimum_life_force:INTEGER = 1-几乎没有生命(非常受伤)
  • 无效:INTEGER = 0-无效
  • 不死生物:整数= -1-最小不死生物(几乎已死)
  • 僵尸:整数= -10-最大不死(非常不死)

从上面的符号常量中,我们开始对D&D游戏中的怪物的活跃,死亡和“过亡”(以及可能的后果或后果)产生心理印象。没有这些词(符号常量),我们剩下的数字范围是-10 .. 10。如果游戏的不同部分依赖于该数字范围对诸如attack_elves或的各种操作的含义,只是没有单词的范围会使我们陷入极大的困惑并可能在我们的游戏中出错seek_magic_healing_potion

因此,在搜索并考虑替换“魔术数字”时,我们想问一些非常有目的性的问题,涉及我们软件范围内的数字,以及数字之间如何在语义上进行交互。

结论

让我们回顾一下我们应该问的问题:

如果您...

  1. 基本值在您的软件领域中可以有特殊含义或目的吗?
  2. 即使在适当的上下文中,特殊含义或目的是否也可能是未知的,不可知的,不清楚的或令人困惑的?
  3. 在错误的环境中,是否可以不恰当地使用适当的基本值而导致不良后果?
  4. 是否可以在适当的情况下正确使用不正确的基本值而带来不良后果?
  5. 基本值在特定上下文中是否与其他基本值具有语义或目的关系?
  6. 基本值是否可以在我们的代码中不止一个地方存在且每个地方都有不同的语义,从而引起读者困惑?

检查代码文本中的独立清单常量基本值。慢慢而又沉思地问每个问题,以此类推。考虑您的回答的强度。很多时候,答案不是黑白的,而是有一些被误解的含义和目的,学习的速度和理解的速度。还需要查看它如何连接到它周围的软件机器。

最后,替换的答案是回答(在您的脑海中)建立连接的读者的强项或弱项(例如“获得”)的量度。他们越了解含义和目的,您就越“神奇”。

结论:仅当魔术足够大而难以检测出由混淆引起的错误时,才用符号常量替换基本值。


1
谢谢。同事不断安装的静态分析工具一直困扰着魔术数字-但是该工具应该如何理解语义呢?结果是将所有基本值都替换为符号常量。当我同意您的结论时,我发现这并不理想。
Chomeh

17

幻数是文件格式或协议交换开始处的字符序列。该号码用作健全性检查。

示例:打开任何GIF文件,您将在开始时看到:GIF89。“ GIF89”是魔术数字。

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

危险是随机二进制数据可能包含这些相同的字符。但这是不太可能的。

对于协议交换,您可以使用它来快速识别正在传递给您的当前“消息”是否已损坏或无效。

幻数仍然有用。


13
我不认为这是一个神奇的数字,他指的
马尔西奥·阿吉亚尔

4
也许您应该删除添加的“文件格式”和“网络”标签,因为他显然不是在谈论那些魔术数字。
兰登

9
知道魔术数字可能不仅仅指代码问题,它仍然非常有用。-亚当
亚当·戴维斯

3
如果主题显示:“在源代码方面,魔幻数字是什么”,则标签不应存在。但是他没有具体说明。因此,拥有我的额外信息是件好事。我认为Kyle,Landon和Marcio是错误的。
Brian R. Bondy

4
也没有办法确定他在寻找哪个人。由于我是第一篇文章,所以我无法猜测他正在寻找哪篇。
Brian R. Bondy

12

在编程中,“幻数”是一个应赋予符号名称的值,但通常作为一个字面量(通常在多个位置)插入到代码中。

出于同样的原因,这很不好,因为SPOT(单点真值)很不错:如果以后要更改此常量,则必须遍历代码以查找每个实例。这也是不好的,因为其他程序员可能不清楚这个数字代表什么,因此是“魔术”。

人们有时通过将这些常量移动到单独的文件中作为配置来进一步消除幻数。有时这是有帮助的,但也可能带来超出其价值的复杂性。


您能更具体地说明为什么消除镁号ISN总是不好吗?
Marcio Aguiar

在数学公式中,例如e ^ pi + 1 = 0
Jared Updike

5
Marcio:执行诸如“ const int EIGHT = 8;”之类的操作时 然后需求发生变化,结果是“ const int EIGHT = 9;”
jmucchiello,2009年

6
抱歉,但这只是命名错误或常量的基本用法的一个示例。
卡扎伊2010年

1
@MarcioAguiar:在某些平台上,像这样的表达式的(foo[i]+foo[i+1]+foo[i+2]+1)/3求值速度可能比循环快得多。如果要在3不将代码重写为循环的情况下替换,则看到ITEMS_TO_AVERAGE定义为的人3可能会认为他们可以将其更改5为代码平均包含更多项。相比之下,用字面量查看表达式的人3会意识到,3表示要加总的项数。
2015年

10

幻数也可以是具有特殊硬编码语义的数字。例如,我曾经看到一个系统,其中记录ID> 0被正常处理,0本身是“新记录”,-1是“这是根”,-99是“这是在根中创建”。0和-99将导致WebService提供新的ID。

这样做的坏处在于您要为特殊功能重新使用空格(记录ID的带符号整数的空格)。也许您永远都不想创建ID为0或ID为负的记录,但即使不是这样,每个看代码或数据库的人也可能会对此感到迷惑,并一开始就感到困惑。毋庸置疑,这些特殊值并没有得到充分记录。

可以说22、7,-12和620也算作魔术数字。;-)


10

使用幻数未曾提及的问题...

如果你有非常多的人,赔率是相当不错的,你有两个不同的目的,其中,您使用幻数,恰好是相同的。

然后,果然,您只需要出于一个目的就更改值。


在谈论数字时,这看起来似乎不太可能(至少对我来说不是),但是我遇到了字符串问题,这很成功:首先,您必须阅读大量代码才能了解其用途。必须注意它正在用于不同的事物...不是我最喜欢的消遣。
Tomislav Nakic-Alfirevic

4

我认为这是对我对您先前问题的回答的回应。在编程中,幻数是一个嵌入的数字常量,没有任何解释。如果它出现在两个不同的位置,则可能导致一个实例被更改而不是另一个实例被更改的情况。由于这两个原因,重要的是在使用它们的地方之外隔离和定义数字常量。


3

我一直用不同的术语“幻数”作为存储在数据结构中的模糊值,可以将其验证为快速有效性检查。例如,gzip文件的前三个字节包含0x1f8b08,Java类文件以0xcafebabe开头,依此类推。

您经常会看到魔术数字嵌入在文件格式中,因为可以随意发送文件,并且丢失有关其创建方式的任何元数据。但是,魔术数字有时也用于内存中的数据结构,例如ioctl()调用。

在处理文件或数据结构之前快速检查幻数可以使人及早发现错误,而不是一路潜入冗长的处理过程中,以便宣布输入是完整的秃头。


2

值得注意的是,有时您确实希望在代码中使用不可配置的“硬编码”数字。有许多著名的算法,其中包括0x5F3759DF,用于优化的平方根反算法。

在极少数情况下,我发现需要使用这样的幻数,我在代码中将它们设置为const,并记录了为什么使用它们,如何工作以及它们来自何处。


1
在我看来,魔术数字代码的气味特别是指无法解释的常数。只要将它们放在命名常量中,就不成问题。
唐·柯比

2

如何在类顶部使用默认值初始化变量?例如:

public class SomeClass {
    private int maxRows = 15000;
    ...
    // Inside another method
    for (int i = 0; i < maxRows; i++) {
        // Do something
    }

    public void setMaxRows(int maxRows) {
        this.maxRows = maxRows;
    }

    public int getMaxRows() {
        return this.maxRows;
    }

在这种情况下,15000是一个幻数(根据CheckStyles)。对我来说,设置默认值是可以的。我不想做:

private static final int DEFAULT_MAX_ROWS = 15000;
private int maxRows = DEFAULT_MAX_ROWS;

这会使阅读起来更困难吗?在安装CheckStyles之前,我从未考虑过这一点。


我认为如果构造函数初始化该值就可以了。否则,如果该值是在构造函数之外初始化的,我只会将其视为麻烦且难以阅读的东西。
Thomas Eding 2010年

我认为static final在一种方法中使用常数时,常数会显得过大。final在方法顶部声明的变量更具可读性,恕我直言。
伊娃(Eva)2013年

0

@ eed3si9n:我什至建议'1'是一个魔术数字。:-)

与幻数相关的一个原则是,您的代码处理的每个事实都应被声明为准确一次。如果您在代码中使用幻数(例如@marcio给出的密码长度示例),则很容易最终复制该事实,并且当您了解该事实更改时,便遇到了维护问题。


5
IOW代码应这样写:factorial n = if n == BASE_CASE then BASE_VALUE else n * factorial (n - RECURSION_INPUT_CHANGE); RECURSION_INPUT_CHANGE = 1; BASE_CASE = 0; BASE_VALUE = 1
Thomas Eding 2010年

0

那么返回变量呢?

我特别发现在实现存储过程时具有挑战性

想象下一个存储过程(我知道错误的语法,只是为了显示一个示例):

int procGetIdCompanyByName(string companyName);

如果它存在于特定表中,则返回公司的ID。否则,它返回-1。不知何故,这是一个神奇的数字。到目前为止,我读过的一些建议说,我确实必须做这样的设计:

int procGetIdCompanyByName(string companyName, bool existsCompany);

顺便说一句,如果公司不存在,应该返回什么?好的:它将将existesCompany设置为false,但也将返回-1。

另一种选择是执行两个单独的功能:

bool procCompanyExists(string companyName);
int procGetIdCompanyByName(string companyName);

因此,第二个存储过程的前提是该公司存在。

但是我担心并发,因为在此系统中,公司可以由另一个用户创建。

顺便说一句,底线是:您如何使用相对已知且可以安全地判断某事不成功或某事不存在的“魔术数”?


在这种特定情况下,如果函数文档说明返回值为负,则表示未找到公司,则没有理由使用常量。
文森特·福尔蒙德

-1

提取幻数作为常数的另一个优点是可以清楚地记录业务信息。

public class Foo {
    /** 
     * Max age in year to get child rate for airline tickets
     * 
     * The value of the constant is {@value}
     */
    public static final int MAX_AGE_FOR_CHILD_RATE = 2;

    public void computeRate() {
         if (person.getAge() < MAX_AGE_FOR_CHILD_RATE) {
               applyChildRate();
         }
    }
}
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.