什么是神奇的数字,为什么它不好?
什么是魔术数字?
为什么要避免?
有没有适合的情况?
幻数是直接使用代码中的数字。
例如,如果你有(用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
, Character
和Math
类的例子。
PS:像FindBugs和PMD这样的静态分析工具会检测代码中使用的幻数,并提出重构的build议。
幻数是一个硬编码的值,可能在稍后阶段发生变化,但难以更新。
例如,假设您有一个页面显示“您的订单”概览页面中的最后50个订单。 50是这里的魔法数字,因为它不是通过标准或惯例来设置的,而是一个由规格中所列出的原因组成的数字。
现在,你所做的是你在不同的地方有50个 – 你的SQL脚本( SELECT TOP 50 * FROM orders
),你的网站(Your Last 50 Orders),你的订单login( for (i = 0; i < 50; i++)
)和其他许多地方。
现在,当有人决定改变50到25时会发生什么? 或75? 或153? 你现在必须在所有的地方取代50个,你很可能会错过它。 查找/replace可能不起作用,因为50可以用于其他的事情,盲目地用50replace50可以有一些其他不良的副作用(即你的Session.Timeout = 50
调用,也设置为25,用户也开始报告频繁超时)。
此外,代码可能很难理解,即“ if a < 50 then bla
” – 如果你遇到一个复杂的函数中,其他开发人员不熟悉的代码可能会问自己“WTF是50? ?”
这就是为什么最好在1个地方有这样一个模棱两可的数字 – “ const int NumOrdersToDisplay = 50
”,因为这会使代码更具可读性(“ if a < NumOrdersToDisplay
”,这也意味着你只需要改变它在1明确的地方。
幻数是适当的地方是通过一个标准定义的一切,即SmtpClient.DefaultPort = 25
或TCPPacketSize = whatever
(不知道这是否标准化)。 而且,只有在1个函数中定义的所有东西都可以接受,但这取决于上下文。
你有没有看过维基百科条目的幻数?
它详细介绍了魔术数字引用的所有方法。 这里有一个关于幻数的引用是一个不好的编程习惯
术语幻数也指在源代码中直接使用数字而没有解释的糟糕的编程实践。 在大多数情况下,这使程序难以阅读,理解和维护。 尽pipe大多数指南对数字零和一个数字进行了例外,但将代码中的所有其他数字定义为命名常量是一个好主意。
幻数是在文件格式或协议交换开始时的一系列字符。 这个号码是一个健康检查。
例如:打开任何GIF文件,您将在开始时看到:GIF89。 “GIF89”是神奇的数字。
其他程序可以读取文件的前几个字符并正确识别GIF。
危险的是随机二进制数据可以包含这些相同的字符。 但这是不太可能的。
至于协议交换,您可以使用它来快速识别传递给您的当前“消息”已损坏或无效。
魔术数字仍然有用。
魔术数字VS. 符号常量:何时replace?
魔术:未知的语义
符号常量 – >提供正确的语义和正确的使用环境
语义:事物的意义或目的。
“创build一个常量,在含义之后命名,并用它replace数字。” – 马丁福勒
首先,幻数不只是数字。 任何基本的价值可以是“魔术”。 基本值是明确的实体,如整数,实数,双精度,浮点数,date,string,布尔值,字符等。 这个问题不是数据types,而是我们的代码文本中出现的值的“神奇”方面。
“魔术”是什么意思? 准确地说:我们打算用“魔术”来指出在我们的代码中上下文的价值的语义(意义或目的) 它是未知的,不可知的,不清楚的或混乱的。 这是“魔术”的概念。 一个基本的价值在其语义或目的存在的时候并不是魔术,而是没有特殊的帮助词(例如象征性的常量)就可以快速而容易地被知道,清楚,理解(而不是混淆)在环境中。
因此,我们通过测量代码阅读器的能力来识别幻数,从而了解其周围环境的基本价值的含义和目的。 读者越less,越不清楚,越困惑,基本价值越“神奇”。
有用的定义
- 迷惑:使(某人)变得迷惑或困惑。
- 困惑:使(某人)困惑和困惑。
- 困惑:完全困惑; 非常困惑。
- 困惑:完全迷惑或困惑。
- 困惑:无法理解; 困惑。
- 理解:理解(词汇,语言或说话者)的意图。
- 意思:一个词,文本,概念或行动的含义。
- 意思是:意图传达,表明或提及(某一事物或观念); 表示。
- 表示:是的表示。
- 指示:指示某事的标志或信息。
- 指出:指出; 显示。
- 标志:存在或出现的对象,质量或事件表明可能存在或出现其他事物。
基本
对于我们的魔法基本价值,我们有两种情况。 只有第二个对程序员和代码来说是最重要的:
- 一个单一的基本价值(如数字),其含义是未知的,不可知的,不清楚的或混乱的。
- 一个基本的价值(如数字)在上下文中,但其含义仍然是未知的,不可知的,不清楚的或混乱的。
“魔术”的总体依赖是单独的基本价值(如数字)如何没有通常已知的语义(如Pi),但是具有本地已知的语义(例如您的程序),这从上下文不完全清楚或可能被滥用在好的或坏的情况下。
大多数编程语言的语义不允许我们使用唯一的基本值,除了(可能)作为数据(即数据表)。 当我们遇到“魔法数字”时,我们通常是在一个语境中这样做的。 因此,答案
“我是否用符号常数replace这个幻数?”
是:
“你在多大程度上能够评估和理解数字的语义含义(它在那里的目的)?”
那种魔法,但不完全
有了这个想法,我们可以很快看到像Pi(3.14159)这样的数字在放置在适当的上下文中(例如2 x 3.14159 x radius或2 * Pi * r)不是一个“幻数”。 在这里,数字3.14159是智力上认可的没有符号常数标识符的Pi。
尽pipe如此,由于数字的长度和复杂性,我们通常用像Pi这样的符号常量标识符replace3.14159。 Pi的长度和复杂性(加上对精度的需求)通常意味着符号标识符或常量不太容易出错。 “丕”这个名字的识别只是一个方便的好处,但并不是持续不断的主要原因。
同时:回到农场
抛开像Pi这样常见的常量,让我们主要关注具有特殊含义的数字,但是这些含义被限制在我们的软件系统的范围之内。 这个数字可能是“2”(作为一个基本的整数值)。
如果我自己使用数字2,我的第一个问题可能是:“2”是什么意思? “2”的意义本身是未知的,没有语境是不可知的,使其使用不清楚和混淆。 即使在我们的软件中只有“2”不会因为语言语义而发生,我们也希望看到“2”本身没有特殊的语义或明显的目的。
让我们把我们唯一的“2”放在: padding := 2
,其中上下文是一个“GUI容器”的上下文。 在这种情况下,2(像素或其他graphics单元)的含义为我们提供了对其语义(含义和目的)的快速猜测。 我们可以在这里停下来,说在这种情况下2是没问题的,除此之外我们没有其他的东西需要知道。 但是,也许在我们的软件领域,这不是全部。 还有更多,但“填充= 2”作为一个上下文无法透露它。
让我们进一步假设在我们的程序中,像素填充2是整个系统中的“default_padding”types。 因此,写入指令padding = 2
是不够好的。 “违约”的概念没有透露。 只有当我写: padding = default_padding
作为一个上下文,然后在其他地方: default_padding = 2
我完全实现了我们的系统中2更好和更全面的含义(语义和目的)。
上面的例子很好,因为“2”本身可以是任何东西。 只有在“我的程序”中将“理解范围”和“理解范围”限制在“我的程序”的GUI UX部分中的default_padding
时,我们才能在合适的上下文中理解“2”。 这里的“2”是一个“魔术”数字,它是在“我的程序”的GUI用户界面的上下文中作为一个符号常量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_elves
或seek_magic_healing_potion
,我们可能会在游戏中出现错误。
因此,在寻找和考虑更换“魔法数字”时,我们想要问的是关于我们软件环境中的数字的充满目的的问题,甚至是这些数字如何在语义上互相影响。
结论
让我们回顾一下我们应该问的问题:
你可能有一个神奇的数字,如果…
- 在你的软件世界中,基本价值是否有特殊的意义或目的?
- 即使在适当的背景下,这个特殊的意义或目的可能是未知的,不可知的,不清楚的或混乱的?
- 在错误的背景下,恰当的基本价值能够被不恰当地使用,造成不好的后果吗?
- 在正确的背景下,不恰当的基本价值能够被正确地使用,并带来不好的后果吗
- 基本价值在特定语境中是否与其他基本价值有语义或目的关系?
- 在我们的代码中,一个基本的值是否可以存在于不同的语义中,从而使我们的读者感到困惑?
检查代码文本中的独立清单常量基本值。 慢慢地,仔细地问每个问题关于这个价值的每个实例。 考虑你的答案的力量。 很多时候,答案不是黑白的,而是有误解的意思和目的,学习的速度和理解的速度。 还需要了解它如何连接到它周围的软件机器。
最后,更换的答案是回答读者做出连接(例如“得到它”)的力量或弱点的措施(在你的脑海中)。 他们理解意义和目的的速度越快,你所拥有的“魔力”就越less。
结论:只有在魔法大到足以导致难以发现混淆所引起的错误时,才用符号常量代替基本值。
在编程中,“幻数”是一个应该赋予符号名称的值,但是作为一个字面值,通常在不止一个地方插入代码中。
同样的原因,SPOT(Single Point of Truth)是不错的:如果你想稍后改变这个常量,你将不得不通过你的代码寻找每一个实例。 这也是不好的,因为其他程序员可能不清楚这个数字代表什么,因此是“魔法”。
人们有时通过将这些常量移到单独的文件中来进行configuration,从而进一步消除幻数。 这有时是有帮助的,但也可能造成更多的复杂性。
一个没有提到使用魔术数字的问题…
如果你有很多这样的数字,那么你有两个不同的用途 ,那就是你使用魔术数字的几率是相当高的。
然后,果然,您只需要为了一个目的而改变价值。
一个幻数也可以是一个具有特殊硬编码语义的数字。 例如,我曾经看到一个系统,其中loggingID> 0被正常处理,0本身是“新logging”,-1是“这是根”,-99是“这是在根中创build的”。 0和-99会导致WebService提供一个新的ID。
有什么不好的是,你正在重用一个空间(loggingID的整数)用于特殊的能力。 也许你永远不会想创build一个ID为0的logging,或者是一个负的ID,但即使不是,每个查看代码或数据库的人都可能会遇到这个问题,并且一开始就感到困惑。 毫无疑问,这些特殊的价值观并没有得到充分的certificate。
可以说,22,7, -12和620也算作魔术数字。 😉
我认为这是对我之前问题的回答。 在编程中,一个幻数是一个embedded的数值常量,不会出现任何解释。 如果出现在两个不同的地点,可能会导致一个实例发生变化的情况,而不是另一个。 由于这两个原因,在他们使用的地方之外隔离和定义数字常量是很重要的。
值得注意的是,有些时候你需要在代码中使用不可configuration的“硬编码”数字。 有一些着名的,包括在优化的逆平方根algorithm中使用的0x5F3759DF。
在极less数情况下,我发现需要使用这样的魔术数字,我把它们设置为一个常量在我的代码中,logging为什么使用它们,它们是如何工作的,以及它们来自哪里。
我一直使用术语“魔术数字”作为不同的数据结构中存储的模糊值,这可以作为一个快速的有效性检查进行validation。 例如,gzip文件包含0x1f8b08作为前三个字节,Java类文件以0xcafebabe开头等等。
您经常会看到以文件格式embedded的幻数,因为文件可能被混乱地发送,并且丢失了有关如何创build的元数据。 然而,幻数也有时用于内存数据结构,如ioctl()调用。
在处理文件或数据结构之前对幻数进行快速检查,可以让人们提前发现错误,而不是通过可能长时间的处理来发布信息,以便宣布input是完整的balderdash。
那么用默认值初始化类顶部的variables呢? 例如:
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:我甚至build议'1'是一个神奇的数字。 🙂
一个与幻数相关的原则是你的代码处理的每一个事实都应该被声明一次。 如果您在代码中使用了幻数(例如@marcio提供的密码长度示例,那么您可能会轻易地重复这一事实,而当您了解这一事实时,就会出现维护问题。
那么返回variables呢?
在执行存储过程时,我特别觉得它很有挑战性
想象下一个存储过程(错误的语法,我知道,只是为了显示一个例子):
int procGetIdCompanyByName(string companyName);
如果它存在于特定的表格中,它将返回公司的Id。 否则,它返回-1。 不知何故,这是一个神奇的数字。 到目前为止,我读过的一些build议说,我真的必须做这样的devise:
int procGetIdCompanyByName(string companyName, bool existsCompany);
顺便说一句,如果公司不存在,应该返回什么? 好吧:它会将existesCompany设置为false ,但也会返回-1。
Antoher选项是使两个独立的function:
bool procCompanyExists(string companyName); int procGetIdCompanyByName(string companyName);
因此,第二个存储过程的前提条件是该公司存在。
但是我害怕并发,因为在这个系统中,一个公司可以由另一个用户创build。
顺便提一下,最后一句话是:你如何看待使用那种相对已知和安全的“神奇数字”来说明一些事情是不成功的或者某种东西不存在的?
提取幻数作为常数的另一个优点是可以清晰地logging商业信息。
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(); } } }