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

什么是神奇数字?

为什么要避免呢?

有没有合适的情况?

369305 次浏览

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

< p >的例子: 打开任何GIF文件,你会在最开始看到:GIF89。"GIF89"是神奇的数字

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

危险在于随机二进制数据可能包含这些相同的字符。但这种可能性非常小。

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

神奇的数字仍然很有用。

你看过维基百科上关于神奇的号码吗?的词条了吗

它详细介绍了魔术数字引用的所有方式。下面是关于魔术数字是一种糟糕的编程实践的引用

“魔数”一词也指在源代码中直接使用数字而不作解释的糟糕编程实践。在大多数情况下,这会使程序更难阅读、理解和维护。尽管大多数指南对数字0和1做了例外处理,但在代码中将所有其他数字定义为命名常量是个好主意。

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

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

例如,假设您有一个页面,在“您的订单”概览页面中显示最近50个订单。50在这里是一个神奇的数字,因为它不是通过标准或惯例设置的,它是您根据规范中概述的原因虚构的数字。

现在,你要做的是你有50个在不同的地方-你的SQL脚本(SELECT TOP 50 * FROM orders),你的网站(你的最后50个订单),你的订单登录(for (i = 0; i < 50; i++))和可能许多其他地方。

那么,如果有人决定把50岁改成25岁,会发生什么呢?还是75年?还是153年?现在你必须在所有的地方替换50,你很可能会错过它。Find/Replace可能不起作用,因为50可能用于其他事情,盲目地将50替换为25可能会产生一些其他不良副作用(例如,您的Session.Timeout = 50调用,它也被设置为25,用户开始报告太频繁的超时)。

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

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

适用于魔法数字的地方是所有通过标准定义的东西,例如SmtpClient.DefaultPort = 25TCPPacketSize = whatever(不确定是否标准化)。此外,只在一个函数中定义的所有内容都可能是可接受的,但这取决于上下文。

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

它不好的原因与SPOT (Single Point of Truth)好的原因是一样的:如果以后想更改这个常量,就必须遍历代码以找到每个实例。这也很糟糕,因为其他程序员可能不清楚这个数字代表什么,因此出现了“魔术”。

人们有时会进一步使用神奇数字消除,将这些常量移到单独的文件中作为配置。这有时是有帮助的,但也会带来更多的复杂性。

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

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

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

在极少数情况下,我发现需要使用这种神奇的数字,我将它们设置为我的代码中的const,并记录为什么使用它们,它们是如何工作的,以及它们来自哪里。

@eed3si9n:我甚至认为“1”是一个神奇的数字。: -)

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

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

您经常看到在文件格式中嵌入了神奇的数字,因为文件可以相当混乱地四处发送,并丢失关于它们如何创建的任何元数据。然而,魔法数字有时也用于内存中的数据结构,如ioctl()调用。

在处理文件或数据结构之前快速检查这个神奇的数字,可以尽早发现错误,而不是为了宣布输入完全是胡编乱作而进行可能很长的处理。

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

这样做的不好之处在于,您重用了用于特殊功能的空间(记录id的有符号整数的空间)。也许您永远不希望创建ID为0或ID为负的记录,但即使不是这样,每个查看代码或数据库的人都可能在一开始无意中发现这一点并感到困惑。不用说,这些特殊的值并没有得到很好的记录。

可以说,22、7、-12和620也是一个神奇的数字。: -)

关于使用神奇数字,还有一个问题没有被提及……

如果你有很多这样的例子,那么你有两个不同的目的,你使用的是魔法数字,而恰好是相同的。

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

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

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

那么返回变量呢?

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

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

int procGetIdCompanyByName(string companyName);

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

int procGetIdCompanyByName(string companyName, bool existsCompany);

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

Antoher的选择是做两个独立的功能

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

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

魔法:语义未知

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

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

“创建一个常数,以其含义命名,并将其替换为数字。”——马丁·福勒

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

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

因此,我们通过测量代码读者从周围上下文了解、清晰和理解基本值的意义和目的的能力来识别神奇数字。读者越不了解,越不清楚,越困惑,就越“神奇”。基本价值是。

基础知识

我们有两种神奇的基本价值观。对于程序员和代码来说,只有第二个才是最重要的:

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

对“魔术”的全面依赖;唯一的基本值(如number)没有常见的语义(如Pi),但有一个局部已知的语义(如你的程序),从上下文中不完全清楚,或者在好或坏的上下文中可能被滥用。

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

“我要用一个符号常数来代替这个神奇的数字吗?”

是:

"你能多快地评估和理解的语义

;

有点魔力,但不完全是

有了这个想法,我们很快就能看到像圆周率(3.14159)这样的数字不是一个“神奇的数字”。当放置在适当的环境中(例如2 x 3.14159 x半径或2πr)。在这里,数字3.14159是没有符号常量标识符的圆周率。

不过,由于数字的长度和复杂性,我们通常将3.14159替换为像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在更大的封闭代码上下文中被快速理解。

因此,任何基本值,其含义(语义和目的)不能被充分和快速地理解,都可以用符号常数来代替基本值(例如幻数)。

要进一步

刻度上的数字也可能有语义。例如,假设我们正在制作一款《龙与地下城》游戏,其中有一个怪物的概念。我们的怪物对象有一个名为life_force的特性,它是一个整数。这些数字的含义如果没有文字来解释,是无法理解或清楚的。因此,我们一开始就武断地说:

  • full_life_force: INTEGER = 10—非常活跃(和未受伤)
  • minimum_life_force: INTEGER = 1—勉强活着(非常受伤)
  • 死亡:INTEGER = 0 -死亡
  • undead: INTEGER = -1—最小undead(几乎死亡)
  • 僵尸:INTEGER = -10—最大不死(非常不死)

从上面的符号常数,我们开始在脑海中勾画出“生”、“死”和“不死”的画面。(以及可能的后果),我们在D&D游戏中的怪物。如果没有这些单词(符号常量),我们就只剩下从-10 .. 10开始的数字。如果游戏的不同部分依赖于数字范围对于不同操作(如attack_elvesseek_magic_healing_potion)的意义,那么没有文字的范围可能会让我们陷入困惑,并在游戏中出现错误。

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

结论

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

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

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

检查代码文本中独立的清单常量基本值。慢慢地、仔细地问每一个问题,每一个例子都有这样的价值。考虑一下你的回答的说服力。很多时候,答案不是非黑即白的,而是有误解的意义和目的、学习速度和理解速度的阴影。还需要了解它是如何与周围的软件机器连接的。

最后,替换的答案是回答(在你的脑海中)读者建立联系的优点或缺点(例如“得到它”)。他们越快理解意义和目的,“魔力”就越少。你已经拥有的。

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