什么是神奇的数字,为什么它不好?

时间:2008-09-06 22:24:25

标签: language-agnostic magic-numbers

什么是幻数?

为什么要避免?

是否存在合适的情况?

15 个答案:

答案 0 :(得分:533)

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

例如,如果你有(在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这样的静态分析工具会检测代码中魔术数字的使用,并建议重构。

答案 1 :(得分:142)

Magic Number是一个硬编码值,可能会在以后更改,但因此很难更新。

例如,假设您有一个页面显示“您的订单”概述页面中的最后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” - 如果您在复杂功能中遇到这种情况,其他不熟悉代码的开发人员可能会问自己“WTF是50 ???“

这就是为什么最好只在一个地方设置这些含糊不清的数字 - “const int NumOrdersToDisplay = 50”,因为这会使代码更具可读性(“if a < NumOrdersToDisplay”,这也意味着你只需要在一个明确定义的地方改变它。

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

答案 2 :(得分:32)

您是否看过magic number?

的维基百科条目

它详细介绍了幻数引用的所有方法。这是关于魔术数字作为一种糟糕的编程习惯的引用

  

术语幻数也指在源代码中直接使用数字而不解释的错误编程实践。在大多数情况下,这会使程序更难以阅读,理解和维护。尽管大多数指南都是数字0和1的例外,但最好将代码中的所有其他数字定义为命名常量。

答案 3 :(得分:23)

魔术数字与符号常数:何时替换?

魔术:未知的语义

符号常数 - &gt;提供正确的语义和正确的使用上下文

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

&#34;创建一个常量,在含义之后命名,并用它替换数字。&#34; - Martin Fowler

首先,魔术数字不仅仅是数字。任何基本价值都可以是&#34;魔术&#34;。基本值是清单实体,例如整数,实数,双精度数,浮点数,日期,字符串,布尔值,字符等。问题不是数据类型,而是&#34;魔术&#34;我们的代码文本中显示的值的方面。

我们的意思是什么&#34;魔术&#34;?确切地说:通过&#34; magic&#34;,我们打算在代码的上下文中指向值的语义(含义或目的);它是未知的,不可知的,不清楚的或令人困惑的。这就是&#34;魔术&#34;的概念。如果没有特殊的帮助词(例如符号常数),那么它的语义或存在目的很快就能很容易地从环绕语境中得知,清晰和理解(不会混淆),那么基本价值就不是魔术。

因此,我们通过测量代码阅读器从周围环境中了解,清楚和理解基本值的含义和目的来识别魔术数字。读者越不知道,越不清楚,越混乱,越多的魔法&#34;基本价值是。

有用的定义

  • 混淆:导致(某人)感到困惑或困惑。
  • 困惑:导致(某人)变得困惑和困惑。
  • 困惑:完全困惑;非常困惑。
  • 困惑:完全迷惑或困惑。
  • 疑惑:无法理解;困惑。
  • 理解:感知(词语,语言或说话者)的意图。
  • 含义:词语,文字,概念或动作的含义。
  • 意思是:打算传达,指示或提及(某一特定事物或概念);表示。
  • 表示:表明。
  • 指示:指示某事物的标志或信息。
  • 表示:指出;秀。
  • sign:一个对象,质量或事件,其存在或发生表明可能存在或发生其他事情。

基本

我们的魔术基本值有两种情景。只有第二个对程序员和代码至关重要:

  1. 一个唯一的基本值(例如数字),其含义未知,不可知,不清楚或混淆。
  2. 上下文中的基本值(例如数字),但其含义仍然未知,不可知,不清楚或混淆。
  3. &#34;魔法&#34;的总体依赖性是唯一的基本值(例如数字)没有通常已知的语义(如Pi),但有一个本地已知的语义(例如你的程序),从上下文中不完全清楚,或者可能在好的或坏的上下文中被滥用(s) )。

    大多数编程语言的语义不允许我们使用单独的基本值,除了(可能)作为数据(即数据表)。当我们遇到&#34;魔术数字&#34;时,我们通常在上下文中这样做。因此,答案

      

    &#34;我是否用符号常量替换这个幻数?&#34;

    是:

      

    &#34;你能多快评估和理解语义的语义   数字(其存在的目的)在其背景下?&#34;

    有点魔法,但不完全

    考虑到这一点,我们可以很快看到像Pi(3.14159)这样的数字是不是一个&#34;幻数&#34;放置在适当的环境中(例如2 x 3.14159 x radius或2 * Pi * r)。这里,数字3.14159是精神上认可的Pi,没有符号常数标识符。

    但是,由于数字的长度和复杂性,我们通常用像Pi这样的符号常量标识符替换3.14159。 Pi的长度和复杂性方面(加上对精度的需求)通常意味着符号标识符或常量不易出错。认识&#34; Pi&#34;作为名称只是一个方便的奖金,但不是持续不变的主要原因。

    同时:回到牧场

    抛开像Pi这样的常见常量,让我们主要关注具有特殊含义的数字,但这些意义仅限于我们软件系统的范围。这样的数字可能是&#34; 2&#34; (作为基本整数值)。

    如果我自己使用数字2,我的第一个问题可能是:&#34; 2&#34;意思? &#34; 2&#34;的含义在没有背景的情况下,它本身是未知的和不可知的,使其使用不清楚和混乱。即使只是&#34; 2&#34;在我们的软件中不会因为语言语义而发生,我们确实希望看到&#34; 2&#34;它本身并没有特殊的语义或明显的目的。

    让我们孤零零的&#34; 2&#34;在上下文中:padding := 2,其中上下文是&#34; GUI容器&#34;。在这种情况下,2的含义(作为像素或其他图形单元)为我们提供了对其语义(意义和目的)的快速猜测。我们可能会在这里停下来说2在这种情况下是可以的,我们不需要知道任何其他事情。然而,也许在我们的软件世界中,这不是整个故事。还有更多,但&#34;填充= 2&#34;因为上下文无法揭示它。

    让我们进一步假设我们程序中的2像素填充是&#34; default_padding&#34;整个系统的多样性。因此,编写指令padding = 2还不够好。 &#34;默认&#34;的概念没有透露。只有当我写作:padding = default_padding作为上下文然后写在其他地方时:default_padding = 2我是否在我们的系统中完全实现了2的更好和更充分的意义(语义和目的)。

    上面的例子非常好,因为&#34; 2&#34;本身可以是任何东西。只有当我们将理解的范围和范围限制在&#34;我的程序&#34;其中2是&#34;我的程序&#34;的GUI UX部分中的default_padding,我们是否最终理解&#34; 2&#34;在适当的背景下。这里&#34; 2&#34;是一个魔术&#34; number,在我的程序&#34;的GUI UX的上下文中被分解为符号常量default_padding。为了使其在封闭代码的更大范围内快速理解为default_padding

    因此,任何基本价值,其含义(语义和目的)都不能被充分和快速地理解,是基本价值(例如幻数)的象征常数的良好候选。

    更进一步

    规模上的数字也可能具有语义。例如,假装我们正在进行D&amp; D游戏,我们有一个怪物的概念。我们的怪物对象有一个名为life_force的功能,它是一个整数。这些数字具有不可知或不清楚的含义,无法提供意义。因此,我们开始任意说:

    • full_life_force:INTEGER = 10 - 非常活着(并且没有受伤)
    • minimum_life_force:INTEGER = 1 - 勉强活着(非常受伤)
    • 死:INTEGER = 0 - 死
    • 亡灵:INTEGER = -1 - 最小不死生物(几乎死亡)
    • zombie:INTEGER = -10 - 最大不死生物(非常不死生物)

    从上面的象征性常数开始,我们开始了解活着,死亡和不死的概念。 (以及可能的后果或后果)我们的D&amp; D游戏中的怪物。如果没有这些词(符号常量),我们只剩下-10 .. 10范围内的数字。如果游戏的不同部分依赖于该范围的数字对于attack_elves或{{1}等各种操作的依赖性,那么只有没有文字的范围会让我们处于可能非常困惑的地方并且可能在游戏中出错。 }}

    因此,在搜索并考虑更换&#34;幻数&#34;我们想问一些关于我们软件环境中数字的充满目的的问题,甚至是数字如何在语义上相互作用。

    结论

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

    如果......你可能有一个神奇的数字。

    1. 基本价值可以在您的软件世界中具有特殊含义或目的吗?
    2. 即使在适当的背景下,特殊意义或目的是否可能是未知的,不可知的,不清楚的或混乱的?
    3. 在错误的背景下,是否可以正确使用适当的基本价值而造成不良后果?
    4. 在正确的背景下,是否可以正确使用不正确的基本价值并产生不良后果?
    5. 基本值是否与特定上下文中的其他基本值具有语义或目的关系?
    6. 我们的代码中不止一个地方存在一个基本值,每个地方都有不同的语义,从而导致我们的读者感到困惑吗?
    7. 检查代码文本中的独立清单常量基本值。每个问题都要缓慢而周密地询问每个问题。考虑你的答案的力量。很多时候,答案不是黑白分明,而是有误解的意义和目的,学习速度和理解速度。还需要了解它如何连接到它周围的软件机器。

      最后,替换的答案是回答(在你的脑海中)读者的力量或弱点的措施(例如&#34;得到它&#34;)。他们越快理解意义和目的,越少&#34;魔法&#34;你有。

      结论:只有当魔法大到足以导致难以发现混乱引起的错误时,才能用符号常量替换基本值。

答案 4 :(得分:17)

幻数是文件格式或协议交换开头的一系列字符。这个号码可用作健全性检查。

实施例: 打开任何GIF文件,您将在一开始就看到:GIF89。 “GIF89”是神奇的数字。

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

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

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

魔术数字仍然有用。

答案 5 :(得分:11)

在编程中,“幻数”是一个应该赋予符号名称的值,而是作为文字插入代码中,通常在多个位置。

同样的原因,SPOT(单点真相)是好的:如果你想稍后更改此常量,你必须搜索代码才能找到每个实例。这也很糟糕,因为其他程序员可能不清楚这个数字代表什么,因此是“魔术”。

人们有时会通过将这些常量移动到单独的文件中作为配置来进一步消除幻数。这有时很有帮助,但也可能造成比它更值得的复杂性。

答案 6 :(得分:10)

使用魔术数字未提及的问题......

如果你有很多这样的人,那么你有两个不同的目的,你正在使用魔术数字,其中恰好发生的几率相当不错是一样的。

然后,当然,您需要更改值...仅用于一个目的。

答案 7 :(得分:9)

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

这有什么不好之处在于你正在重用一个特殊能力的空间(记录ID的有符号整数)。也许你永远不会想要创建ID为0或带有负ID的记录,但即使不是这样,每个看过代码或数据库的人都可能偶然发现并且最初会感到困惑。不言而喻,这些特殊的价值观没有得到很好的记录。

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

答案 8 :(得分:4)

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

答案 9 :(得分:3)

值得注意的是,有时您需要在代码中使用不可配置的“硬编码”数字。有许多famous ones包括0x5F3759DF,用于优化的反平方根算法。

在极少数情况下,我发现需要使用这样的Magic Numbers,我在代码中将它们设置为const,并记录它们的使用原因,工作原理以及它们的来源。

答案 10 :(得分:3)

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

您经常会看到以文件格式嵌入的幻数,因为文件可以相当混杂地发送,并丢失有关它们如何创建的元数据。然而,幻数也有时用于内存数据结构,如ioctl()调用。

在处理文件或数据结构之前快速检查幻数允许人们提前发出错误信号,而不是一直处理可能冗长的处理,以宣布输入是完整的balderdash。

答案 11 :(得分:3)

使用默认值初始化类顶部的变量怎么样?例如:

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之前,我从未考虑过这个问题。

答案 12 :(得分:1)

@ eed3si9n:我甚至建议'1'是一个神奇的数字。 : - )

与幻数相关的原则是,您的代码处理的每个事实都应该被声明一次。如果你在代码中使用幻数(例如@marcio提供的密码长度示例,你很容易就会重复这个事实,当你对这个事实的理解发生变化时,你就会遇到维护问题。

答案 13 :(得分:1)

返回变量怎么样?

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

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

int procGetIdCompanyByName(string companyName);

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

int procGetIdCompanyByName(string companyName, bool existsCompany);

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

Antoher选项是制作两个单独的函数:

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

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

但我害怕并发,因为在这个系统中,公司可以由另一个用户创建。

顺便说一句,最重要的是:您如何看待使用那种相对已知且安全的“神奇数字”来判断某些事情是不成功的还是某些事情不存在?

答案 14 :(得分:0)

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

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