【问题标题】:What is a magic number, and why is it bad? [closed]什么是幻数,为什么它不好? [关闭]
【发布时间】:2010-09-08 01:16:46
【问题描述】:

什么是幻数?

为什么要避免?

有合适的情况吗?

【问题讨论】:

  • 您将避免使用幻数,因为其他人查看您的代码可能无法理解您为什么要这样做……例如const myNum = 22; const number = myNum / 11; 现在我的 11 可能是人或啤酒瓶或其他东西,所以我会将 11 更改为常数,例如居民。
  • 在属性中使用幻数是不可避免的,所以我想这是合适的。
  • 这里有很多关于使用 magin number 的讨论,但是为什么不进行更广泛的讨论来涵盖其他“魔术”常量的使用,比如一个永远不会改变的字符串来定义一个例如键入。使用它是一种好习惯,还是会损害可读性?

标签: language-agnostic terminology magic-numbers


【解决方案1】:

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

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

【讨论】:

  • 0 和 1 是此规则的例外。
  • @Kirill:如果您期望“百分之一百”的定义会发生变化,那么是的。更好的方法是将变量从它的本质变为它所代表的,即 public static final MAX_DOWNLOAD_PERCENTAGE = 100。尽管即使这样也没有意义,因为“100%”的定义非常明确。另一方面,Passwords 最长为 7 个字符这一事实并不是全局定义的,实际上是不同的,因此这是一个变量的候选者。
  • @Jonathan Parker,除非他们不是 (TRUE/FALSE)
  • 魔数永远不会改变并不意味着它不应该被一个常数代替。我的代码充满了诸如 HzPerMHz 和 msecPerSecond 之类的全局常量。这些永远不会改变,但它们使含义更清晰,并提供了一些防止拼写错误的保护。
  • @MarcusJ 你大错特错了。这不是意见问题,而是许多程序员来之不易的经验。我说不清,在过去的 40 年编程中,我诅咒过多少次没有定义常量的以前的程序员,所以我才发现直接使用数字,需要在代码维护时理解, 埋在很多代码的某个地方,通过定义这样一个常量,它的含义就很清楚了。任何其他高级程序员也会有许多类似的恐怖故事。
【解决方案2】:

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

例如,假设您有一个页面,其中显示“您的订单”概览页面中的最后 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 个函数中定义的所有内容可能都是可以接受的,但这取决于上下文。

【讨论】:

  • 即使它不能改变它仍然是一个坏主意,因为不清楚发生了什么。
  • 并不总是不清楚。 SmtpClient.DefaultPort = 25 可能比SmtpClient.DefaultPort = DEFAULT_SMTP_PORT 更清楚er
  • @immibis 我想这是假设绝对没有其他代码使用 DEFAULT_SMTP_PORT 的概念。如果该应用程序的默认 SMTP 端口发生更改,则需要在多个位置进行更新,从而可能导致不一致。
  • 查找所有用法也更难——您必须在整个应用程序中搜索25,并确保您只更改用于 SMTP 端口的 25,而不是25 是例如表格列的宽度或页面上显示的记录数。
  • 在那个例子中,我希望代码使用 SmtpClient.DefaultPort,而不是 25。所以你只需要在一个地方改变它。而且端口号很可能保持不变,不是随机幻数,而是IANA分配的数字。
【解决方案3】:

您是否查看过magic number? 的维基百科条目

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

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

【讨论】:

  • RTFW 的好例子 :)
  • 我想说答案还远远不够。
【解决方案4】:

幻数对比。符号常量:何时替换?

魔法:未知语义

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

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

“创建一个常量,根据含义命名,并用它替换数字。” ——马丁·福勒

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

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

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

有用的定义

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

基础知识

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

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

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

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

的答案

“我是否将这个幻数替换为符号常数?”

是:

“你能多快评估和理解 其上下文中的数字(其存在的目的)?”

有点魔法,但不完全是

考虑到这一点,我们可以很快看到像 Pi (3.14159) 这样的数字在放置在适当的上下文中时如何不是“幻数”(例如 2 x 3.14159 x 半径或 2*Pi*r)。这里,数字 3.14159 是心理识别的 Pi,没有符号常量标识符。

由于数字的长度和复杂性,我们通常将 3.14159 替换为像 Pi 这样的符号常量标识符。 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 是“我的程序”的 GUI UX 部分中的default_padding 时,我们才能最终在适当的上下文中理解“2”。这里“2”是一个“神奇”数字,它在“我的程序”的 GUI UX 上下文中被分解为符号常量 default_padding,以便在更大的上下文中快速理解为 default_padding的封闭代码。

因此,任何其含义(语义和目的)不能被充分和快速理解的基本值都是一个很好的候选符号常量来代替基本值(例如幻数)。

走得更远

刻度上的数字也可能具有语义。例如,假设我们正在制作一款 D&D 游戏,其中我们有怪物的概念。我们的怪物对象有一个名为life_force 的特征,它是一个整数。这些数字的含义是不可知的或没有文字来提供含义的清晰。因此,我们开始武断地说:

  • full_life_force: INTEGER = 10 -- 非常活跃(并且没有受伤)
  • minimum_life_force: INTEGER = 1 -- 勉强活着(非常受伤)
  • 已死:INTEGER = 0 -- 已死
  • 不死生物:INTEGER = -1 -- 最少不死生物(几乎死了)
  • 僵尸:INTEGER = -10 -- 最大不死生物(非常不死生物)

从上面的符号常数,我们开始对我们的 D&D 游戏中的怪物的活、死和“不死”(以及可能的后果或后果)有了一个心理图景。没有这些词(符号常量),我们只剩下从-10 .. 10 范围内的数字。如果游戏的不同部分依赖于数字范围对各种操作(如attack_elvesseek_magic_healing_potion)的含义,那么没有单词的范围就会让我们处于一个可能非常混乱的地方,并且可能在我们的游戏中出现错误。

因此,在搜索和考虑替换“幻数”时,我们希望就软件上下文中的数字提出非常有目的性的问题,甚至这些数字在语义上如何相互作用。

结论

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

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

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

检查代码文本中的独立清单常量基本值。慢慢地、深思熟虑地询问每一个关于这种价值的实例的问题。考虑你的答案的强度。很多时候,答案不是非黑即白,而是带有被误解的含义和目的、学习速度和理解速度的阴影。还需要看看它是如何连接到它周围的软件机器上的。

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

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

【讨论】:

  • 谢谢。 Fwiw 我的同事一直在安装的静态分析工具一直在抱怨幻数——但是一个工具应该如何理解语义呢?结果是所有基本值都被符号常量替换。我同意你的结论,我觉得这不太理想。
【解决方案5】:

幻数是文件格式或协议交换开头的字符序列。此数字用作完整性检查。

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

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

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

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

幻数仍然有用。

【讨论】:

  • 我认为这不是他所指的神奇数字
  • 也许你应该删除你添加的“文件格式”和“网络”标签,因为他显然不是在谈论那些神奇的数字。
  • 知道幻数可能不仅仅指代码问题,这仍然非常有用。 -亚当
  • 如果主题阅读:“就源代码而言,什么是幻数”,那么标签不应该在那里。但他没有具体说明这一点。所以有我的额外信息很好。我认为凯尔、兰登和马西奥错了。
  • 也无法确定他在寻找哪一个。因为我是第一个帖子,所以我猜不出他在找哪个。
【解决方案6】:

在编程中,“幻数”是一个应该被赋予符号名称的值,但它却作为文字滑入代码中,通常在多个地方。

这很糟糕,原因与 SPOT(Single Point of Truth)的优点相同:如果您想稍后更改此常量,则必须在代码中寻找每个实例。这也很糟糕,因为其他程序员可能不清楚这个数字代表什么,因此是“魔法”。

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

【讨论】:

  • 您能否更具体地说明为什么消除魔数并不总是好的?
  • 在 e^pi + 1 = 0 等数学公式中
  • Marcio:当您执行“const int EIGHT = 8;”之类的操作时然后需求发生变化,最终得到“const int EIGHT = 9;”
  • 抱歉,这只是一个错误命名的例子,或者是常量的基本用法。
  • @MarcioAguiar:在某些平台上,像(foo[i]+foo[i+1]+foo[i+2]+1)/3 这样的表达式的计算速度可能比循环快得多。如果要替换 3 而不将代码重写为循环,那么看到 ITEMS_TO_AVERAGE 定义为 3 的人可能会认为他们可以将其更改为 5 并让代码平均更多项。相比之下,使用文字 3 查看表达式的人会意识到 3 表示相加的项目数。
【解决方案7】:

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

如果你有很多,那么很有可能你有两个不同的目的你正在使用幻数,恰好发生在一样。

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

【讨论】:

  • 在谈论数字时这看起来不太可能(至少对我来说不是),但我用字符串遇到了它,它很成功:首先你必须阅读大量代码要看它在哪里使用,而不是你必须注意到它被用于不同的事情......不是我最喜欢的消遣。
【解决方案8】:

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

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

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

【讨论】:

    【解决方案9】:

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

    【讨论】:

      【解决方案10】:

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

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

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

      【讨论】:

        【解决方案11】:

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

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

        【讨论】:

        • 在我看来,神奇的数字代码气味专门指的是 unexplained 常量。只要你把它们放在一个命名常量中,这应该不是问题。
        【解决方案12】:

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

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

        【讨论】:

        • 我认为如果构造函数初始化值就可以了。否则,如果值是在构造函数之外初始化的,我只会认为它很麻烦,而且更难阅读。
        • 我认为static final 常量在您以一种方法使用它们时是矫枉过正的。在方法顶部声明的 final 变量在恕我直言时更具可读性。
        【解决方案13】:

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

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

        【讨论】:

        • 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
        【解决方案14】:

        返回变量呢?

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

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

        int procGetIdCompanyByName(string companyName);
        

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

        int procGetIdCompanyByName(string companyName, bool existsCompany);
        

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

        另一个选项是创建两个单独的函数:

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

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

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

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

        【讨论】:

        • 在这种特定情况下,如果函数的文档指出负返回值表示未找到公司,则没有理由使用常量。
        【解决方案15】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-06
          • 1970-01-01
          • 2017-12-07
          相关资源
          最近更新 更多