关于语言不可知:什么是神奇的数字,为什么它不好?

关于语言不可知:什么是神奇的数字,为什么它不好?

What is a magic number, and why is it bad?

什么是幻数?

为什么要避免?

是否有合适的案例?


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

例如,如果你有(在爪哇):

1
2
3
4
5
6
7
8
public class Foo {
    public void setPassword(String password) {
         // don't do this
         if (password.length() > 7) {
              throw new InvalidArgumentException("password");
         }
    }
}

应将其重构为:

1
2
3
4
5
6
7
8
9
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)可以检测代码中使用的幻数,并建议进行重构。


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

例如,假设您有一个页面,在"您的订单"概述页面中显示最后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?"??"

这就是为什么最好在一个地方有这样的模糊和任意的数字——"const int NumOrdersToDisplay = 50",因为这使代码更易读("if a < NumOrdersToDisplay",这也意味着你只需要在一个定义良好的地方更改它。

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


你看过维基百科的魔法数字条目了吗?

它详细介绍了magic数字引用的所有方法。这里有一个关于幻数的引述,它是一种糟糕的编程实践

The term magic number also refers to the bad programming practice of using numbers directly in source code without explanation. In most cases this makes programs harder to read, understand, and maintain. Although most guides make an exception for the numbers zero and one, it is a good idea to define all other numbers in code as named constants.


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

魔法:未知语义好的。

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

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

"创建一个常量,按其含义命名,并用它替换数字。"——马丁·福勒好的。

首先,神奇的数字不仅仅是数字。任何基本价值都可以是"魔法"。基本值是表示实体,如整数、实数、双精度、浮点数、日期、字符串、布尔值、字符等。问题不是数据类型,而是值在代码文本中出现的"魔力"方面。好的。

"魔法"是什么意思?准确地说:通过"魔法",我们打算指出代码上下文中的值的语义(含义或目的);它是未知的、不可知的、不清楚的或混淆的。这就是"魔法"的概念。当基本值的语义含义或存在的目的在没有特殊帮助词(如符号常量)的情况下从环绕上下文中快速、容易地被了解、清晰和理解(不混淆)时,它就不是魔法。好的。

因此,我们通过测量代码阅读器从周围环境中了解、清楚和理解基本值的含义和目的的能力来识别幻数。读者越不了解、越不清楚、越困惑,其基本价值就越"神奇"。好的。有用的定义

  • 使困惑:使(某人)变得困惑或困惑。
  • 困惑:使(某人)变得困惑和困惑。
  • 困惑:完全困惑;非常困惑。
  • 困惑:完全困惑或困惑。
  • 困惑:无法理解;困惑。
  • 理解:理解(单词、语言或说话者)的意图。
  • 意思:单词、文本、概念或动作的意思。
  • 意思:打算传达、表明或提及(一个特定的事物或概念);表示。
  • 表示:表示。
  • 指示:指示某物的标志或信息。
  • 表示:指出;显示。
  • 符号:一个物体、质量或事件,其存在或发生表示可能存在或发生其他事物。

基础

我们有两个魔术基本价值的场景。只有第二个对程序员和代码最重要:好的。

  • 一个单独的基本值(如数字),其含义未知、不可知、不清楚或混淆。
  • 上下文中的一个基本值(如数字),但其含义仍然未知、不可知、不清楚或混淆。
  • "magic"的总体依赖性是指单个基本值(例如数字)没有常见的语义(如pi),但有一个本地已知的语义(例如程序),它从上下文中不完全清楚,或者可能在好或坏的上下文中被滥用。好的。

    大多数编程语言的语义将不允许我们使用单独的基本值,除了(可能)作为数据(即数据表)。当我们遇到"神奇数字"时,我们通常在一个上下文中这样做。因此,答案是好的。

    "Do I replace this magic number with a symbolic constant?"

    Ok.

    是:好的。

    "How quickly can you assess and understand the semantic meaning of the
    number (its purpose for being there) in its context?"

    Ok.

    有点神奇,但不完全是

    考虑到这一点,我们可以很快看到像pi(3.14159)这样的数字在适当的上下文中(例如2 x 3.14159 x radius或2*pi*r)不是"幻数"。在这里,数字3.14159是不带符号常量标识符的思想上可识别的π。好的。

    不过,由于数字的长度和复杂性,我们通常用符号常量标识符(如pi)替换3.14159。pi的长度和复杂性方面(加上对准确性的需求)通常意味着符号标识符或常量不太容易出错。将"pi"识别为一个名称只是一个方便的奖励,但并不是拥有常量的主要原因。好的。同时:回到牧场

    抛开像pi这样的常见常量,让我们主要关注具有特殊含义的数字,但这些含义仅限于我们软件系统的宇宙。这样的数字可能是"2"(作为基本整数值)。好的。

    如果我单独使用数字2,我的第一个问题可能是:"2"是什么意思?"2"本身的含义是未知的,没有上下文是不可知的,它的使用不清楚和混乱。尽管在我们的软件中只有"2"不会因为语言语义而发生,但我们确实希望看到"2"本身没有特殊的语义或明显的目的。好的。

    让我们把我们唯一的"2"放在:padding := 2的上下文中,其中上下文是一个"gui容器"。在这种情况下,2的含义(作为像素或其他图形单位)为我们提供了对其语义(含义和目的)的快速猜测。我们可以在这里停下来说,在这种情况下,2是可以的,我们不需要知道其他任何事情。然而,也许在我们的软件领域,这不是全部。还有更多的内容,但是"padding=2"作为上下文不能显示它。好的。

    让我们进一步假设在我们的程序中,2作为像素填充在整个系统中是"默认填充"类型。因此,编写padding = 2指令是不够的。"违约"的概念没有被揭示出来。只有当我写:padding = default_padding作为上下文,然后在其他地方:default_padding = 2时,我才能在我们的系统中充分认识到2的更好和更完整的含义(语义和目的)。好的。

    上面的例子很好,因为"2"本身可以是任何东西。只有当我们把理解的范围和领域限制在"我的程序"中,其中2是"我的程序"的GUI UX部分中的default_padding,我们才能在适当的上下文中理解"2"。这里的"2"是一个"神奇"数字,它被分解成一个符号常量default_padding,在"我的程序"的gui-ux上下文中,以便使其在封闭代码的更大上下文中快速理解为default_padding。好的。

    因此,任何基本值,其意义(语义和目的)不能充分和迅速地理解,是一个很好的候选者,一个符号常数在基本值(如幻数)的地方。好的。走得更远

    规模上的数字也可能有语义。例如,假设我们正在制作一个D&D游戏,在这个游戏中我们有一个怪物的概念。我们的怪物有一个叫做life_force的特征,它是一个整数。数字的含义是不可知的或不清楚的,没有文字来提供意义。因此,我们从武断地说:好的。

    • 全寿命力:整数=10——非常有活力(并且没有受伤)
    • 最小生存力:整数=1——勉强活着(非常受伤)
    • 死:整数=0--死
    • undead:integer=-1--最小undead(几乎死了)
    • 僵尸:整数=-10--最大不死(非常不死)

    从上面的符号常量中,我们开始从精神上了解D&D游戏中怪物的活力、死亡和"不适应"(以及可能的后果)。如果没有这些词(符号常量),我们只剩下-10 .. 10中的数字。如果游戏的不同部分依赖于数字范围对各种操作(如attack_elvesseek_magic_healing_potion)的意义,那么没有单词的范围就可能使我们处于一个可能非常混乱的位置,并可能在游戏中出现错误。好的。

    因此,在搜索和考虑替换"幻数"时,我们想问一些非常有目的的问题,关于我们软件环境中的数字,甚至是数字如何在语义上相互作用。好的。结论

    让我们回顾一下我们应该问哪些问题:好的。

    如果……好的。

  • 在你的软件世界中,基本价值能有一个特殊的意义或目的吗?
  • 即使在适当的上下文中,特殊的含义或目的可能是未知的、不可知的、不清楚的或混淆的吗?
  • 在错误的上下文中,是否可以不正确地使用适当的基本值并产生不良后果?
  • 在正确的上下文中,是否可以正确地使用不正确的基本值并带来不良后果?
  • 基本值是否与特定上下文中的其他基本值存在语义或目的关系?
  • 在我们的代码中,一个基本值是否可以存在于多个不同语义的地方,从而导致我们的读者困惑?
  • 检查代码文本中的独立清单常量基本值。对于这样一个值的每个实例,缓慢而深思熟虑地问每个问题。考虑一下你答案的力度。很多时候,答案不是黑白的,而是有误解的意义和目的、学习的速度和理解的速度。还需要了解它如何连接到周围的软件机器。好的。

    最后,替换的答案是回答(在你的头脑中)读者的强弱来建立联系(例如"得到它")。他们越快理解意义和目的,你就越不具有"魔力"。好的。

    结论:只有当魔法足够大,难以检测到由混乱引起的错误时,才用符号常量替换基本值。好的。好啊。


    幻数是文件格式或协议交换开始时的字符序列。这个数字是一个健全的检查。

    例子:打开任意一个GIF文件,您将在一开始看到:gif89。""是神奇的数字。

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

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

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

    神奇的数字仍然有用。


    在编程中,"magic number"是一个应该被赋予符号名的值,但它被作为一个文本滑入代码中,通常位于多个位置。

    出于同样的原因,spot(单点真理)是好的,这是不好的:如果您以后想更改这个常量,您必须搜索代码来查找每个实例。这也很糟糕,因为其他程序员可能不清楚这个数字代表什么,因此"魔法"。

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


    一个还没有提到的使用魔法数字的问题…

    如果你有很多,你有两个不同的目的,你使用魔法数字的概率是相当不错的,在那里值恰好是相同的。

    当然,你需要改变这个值…只有一个目的。


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

    这有什么不好的地方,就是你要重用一个空间(有符号整数作为记录ID)来获得特殊的能力。也许您永远不想创建ID为0或负ID的记录,但即使不是这样,查看代码或数据库的每个人都可能会无意中发现这一点,并且一开始会感到困惑。毫无疑问,这些特殊的价值观没有很好的记录。

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


    我想这是我对你先前问题的回答。在编程中,幻数是一个嵌入的数字常量,它没有解释就出现了。如果它出现在两个不同的位置,则可能导致一个实例发生更改,而不是另一个实例。出于这两个原因,隔离和定义使用它们的地方之外的数值常数是很重要的。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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是一个神奇的数字(根据支票样式)。对我来说,设置默认值是可以的。我不想这样做:

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

    这会使阅读变得更困难吗?直到我安装了CheckStyles,我才考虑过这个问题。


    我一直用"幻数"这个词来表示不同的含义,它是一个存储在数据结构中的模糊值,可以作为快速有效性检查进行验证。例如,GZIP文件包含0x1F8B08作为它们的前三个字节,Java类文件以0xCabeBabe等开始。

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

    在处理文件或数据结构之前,对幻数进行快速检查,可以让人尽早发出错误信号,而不是一路拖来拖去可能漫长的处理过程,以宣布输入完全是胡说八道。


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

    在我发现需要使用这种神奇数字的罕见情况下,我将它们设置为代码中的常量,并记录它们的使用原因、工作方式和来源。


    返回变量呢?

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

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

    1
    int procGetIdCompanyByName(string companyName);

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

    1
    int procGetIdCompanyByName(string companyName, bool existsCompany);

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

    安托弗的选择是做两个独立的功能:

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

    所以第二个存储过程的前提条件是公司存在。

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

    顺便说一句,底线是:你怎么看待使用那种相对已知和安全的"神奇数字"来判断某件事不成功或某件事不存在?


    @EED3SI9N:我甚至认为‘1’是一个神奇的数字。-)

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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();
             }
        }
    }


    推荐阅读