什么是神奇数字?

为什么要避免它?

有合适的情况吗?

有帮助吗?

解决方案

幻数是在代码中直接使用数字。

例如,如果您有(在 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, CharacterMath 类。

附:FindBugs 和 PMD 等静态分析工具可检测代码中幻数的使用并提出重构建议。

其他提示

幻数是一个硬编码值,可能会在稍后阶段发生变化,但因此很难更新。

例如,假设您有一个页面在“您的订单”概览页面中显示最后 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 call,也设置为 25,用户开始报告超时太频繁)。

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

这就是为什么最好将如此模糊且任意的数字恰好放在 1 个位置 - ”const int NumOrdersToDisplay = 50”,因为这使得代码更具可读性(“if a < NumOrdersToDisplay”,这也意味着您只需在 1 个明确定义的位置进行更改。

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

你看过维基百科的条目吗 神奇的数字?

它详细介绍了幻数引用的所有制作方式。这是关于魔幻数字是一种糟糕的编程实践的引述

术语“幻数”还指在源代码中直接使用数字而不进行解释的不良编程习惯。在大多数情况下,这会使程序更难阅读、理解和维护。尽管大多数指南都对数字 0 和 1 进行了例外处理,但最好将代码中的所有其他数字定义为命名常量。

魔法数字VS。符号常数:什么时候更换?

魔法:未知语义

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

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

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

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

我们所说的“魔法”是什么意思?准确地说:通过“魔法”,我们打算指出代码上下文中值的语义(含义或目的);它是未知的、不可知的、不清楚的或令人困惑的。这就是“魔法”的概念。当基本价值的语义意义或存在目的可以从周围的上下文中快速、容易地知道、清晰和理解(不混淆)而无需特殊的辅助词(例如,“”)时,它就不是魔法了。符号常数)。

因此,我们通过测量读码器从周围环境中了解、清楚和理解基本值的含义和目的的能力来识别幻数。读者越不为人所知、越不清楚、越困惑,基本价值就越“神奇”。

有用的定义

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

基本

我们的魔法基本值有两种情况。对于程序员和代码来说,只有第二个才是最重要的:

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

“魔法”的首要依赖是单独的基本价值(例如number)没有众所周知的语义(如 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, ,这是一个整数。如果没有文字来提供意义,这些数字的含义是不可知或不清楚的。因此,我们首先武断地说:

  • 完整生命力:INTEGER = 10 -- 还活着(并且没有受伤)
  • 最小生命力:INTEGER = 1 -- 勉强活着(非常受伤)
  • 死的:整数 = 0 -- 死
  • 不死族:INTEGER = -1 -- 最小不死生物(几乎死亡)
  • 僵尸:INTEGER = -10 -- 最大不死族(非常不死族)

从上面的符号常量中,我们开始在 D&D 游戏中对怪物的存活、死亡和“不死”(以及可能的后果或后果)有一个清晰的认识。如果没有这些单词(符号常量),我们只剩下范围从 -10 .. 10. 。如果游戏的不同部分依赖于该数字范围对各种操作的含义,那么仅没有单词的范围就可能会让我们陷入极大的混乱,并且可能会在游戏中出现错误 attack_elves 或者 seek_magic_healing_potion.

因此,在搜索和考虑替换“幻数”时,我们想要询问有关我们软件上下文中的数字的非常有目的的问题,甚至这些数字如何在语义上相互交互。

结论

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

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

  1. 基本值在您的软件世界中是否具有特殊含义或目的?
  2. 即使在适当的上下文中,特殊含义或目的是否可能是未知的、不可知的、不清楚的或令人困惑的?
  3. 正确的基本价值观在错误的背景下使用不当会产生不良后果吗?
  4. 不正确的基本价值观能否在正确的背景下被正确使用而产生不良后果?
  5. 基本价值观与特定上下文中的其他基本价值观是否具有语义或目的关系?
  6. 一个基本值是否可以存在于代码中的多个位置,并且每个位置具有不同的语义,从而导致读者感到困惑?

检查代码文本中的独立清单常量基本值。慢慢地、深思熟虑地询问关于此类值的每个实例的每个问题。考虑一下你的答案的强度。很多时候,答案并不是黑白分明的,而是有一些被误解的意义和目的、学习速度和理解速度。还需要看看它如何与周围的软件机器连接。

最后,替换的答案是回答(在你心中)读者建立联系的优势或劣势的衡量标准(例如,“得到它”)。他们越快理解意义和目的,你的“魔力”就越少。

结论:仅当魔力大到足以导致难以检测由于混乱而产生的错误时,才用符号常量替换基本值。

幻数是文件格式或协议交换开头的字符序列。这个数字可以作为健全性检查。

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

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

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

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

魔法数字仍然有用。

在编程中,“幻数”是一个应该被赋予符号名称的值,但实际上却以文字形式滑入代码中,通常出现在多个位置。

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

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

使用幻数时没有提到的一个问题......

如果你有很多,那么你有两个不同的可能性相当大 目的 你正在使用幻数,其中 价值观 碰巧是一样的。

然后,果然,你需要改变这个值......仅用于一个目的。

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

这样做的不好之处在于,您会重复使用空格(用于记录 ID 的有符号整数的空格)来实现特殊功能。也许您永远不想创建 ID 为 0 或负 ID 的记录,但即使不想,每个查看代码或数据库的人都可能会偶然发现这一点,并且一开始会感到困惑。不用说,这些特殊值没有得到充分记录。

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

我认为这是对我的回应 回答 对于你之前的问题。在编程中,幻数是一个嵌入的数值常量,无需解释即可出现。如果它出现在两个不同的位置,则可能会导致一个实例发生更改而另一个实例不变的情况。出于这两个原因,在使用数值常量的地方之外隔离和定义数值常量非常重要。

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

在极少数情况下,我发现需要使用此类幻数,我将它们设置为代码中的常量,并记录它们的使用原因、它们的工作原理以及它们的来源。

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

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

在处理文件或数据结构之前快速检查幻数可以让人们及早发出错误信号,而不是一路拖着可能冗长的处理来宣布输入完全是胡言乱语。

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

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 之前我从未考虑过这一点。

@eed3si9n:我什至建议“1”是一个神奇的数字。:-)

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

返回变量呢?

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

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

int procGetIdCompanyByName(string companyName);

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

int procGetIdCompanyByName(string companyName, bool existsCompany);

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

另一个选择是创建两个独立的函数:

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

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

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

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

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

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();
         }
    }
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top