Answers:
幻数是代码中数字的直接用法。
例如,如果您拥有(使用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之类的静态分析工具会检测代码中幻数的使用,并提出重构建议。
TRUE
/ FALSE
)
幻数是一个硬编码的值,它可能在以后的阶段中更改,但是因此可能很难更新。
例如,假设您有一个页面显示“您的订单”概述页面中的最后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 = 25
或TCPPacketSize = whatever
(不确定是否标准化)。同样,仅在1个函数内定义的所有内容都可以接受,但这取决于上下文。
SmtpClient.DefaultPort = 25
可能是明显的ER比SmtpClient.DefaultPort = DEFAULT_SMTP_PORT
。
25
在整个应用程序中进行搜索,并确保仅更改25
SMTP端口的出现,而不是更改25的出现(例如表列的宽度或数字)记录显示在页面上。
IANA
。
您是否已查看Wikipedia条目中的魔术数字?
它详细介绍了魔术数字引用的所有创建方式。这是关于魔术数作为一种不好的编程习惯的报价
魔术数字一词还指不正确地直接在源代码中使用数字的不良编程习惯。在大多数情况下,这会使程序更难以阅读,理解和维护。尽管大多数指南都将数字零和一作为例外,但是将代码中的所有其他数字定义为命名常量是个好主意。
魔术:未知语义
符号常量->提供正确的语义和正确的使用上下文
语义:事物的意义或目的。
“创建一个常量,在含义后命名,然后用它替换数字。” -马丁·福勒(Martin Fowler)
首先,魔术数字不仅仅是数字。任何基本值都可以是“魔术”。基本值是清单实体,例如整数,实数,双精度数,浮点数,日期,字符串,布尔值,字符等。问题不在于数据类型,而在于代码文本中出现的值的“魔幻”方面。
“魔术”是什么意思?确切地说:通过“魔术”,我们打算在代码的上下文中指向值的语义(含义或目的);它是未知的,不可知的,不清楚的或令人困惑的。这就是“魔术”的概念。当基本含义的语义或存在目的在没有特殊辅助词(例如符号常量)的情况下,可以从周围环境中快速,轻松地了解,清楚和理解(不会混淆)时,就不是魔术。
因此,我们通过测量代码阅读器从周围环境中了解,清楚并理解基本值的含义和目的的能力,来确定幻数。读者越不了解,不清楚和混乱,其基本价值就越“神奇”。
我们为魔术基本值提供了两种方案。对于程序员和代码,只有第二个是最重要的:
“魔术”的总体依赖性是孤单的基本值(例如,数字)如何不具有众所周知的语义(例如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
,它是一个整数。这些数字的含义是无法理解或清楚的,没有文字可以提供含义。因此,我们首先随意说:
从上面的符号常量中,我们开始对D&D游戏中的怪物的活跃,死亡和“过亡”(以及可能的后果或后果)产生心理印象。没有这些词(符号常量),我们剩下的数字范围是-10 .. 10
。如果游戏的不同部分依赖于该数字范围对诸如attack_elves
或的各种操作的含义,只是没有单词的范围会使我们陷入极大的困惑并可能在我们的游戏中出错seek_magic_healing_potion
。
因此,在搜索并考虑替换“魔术数字”时,我们想问一些非常有目的性的问题,涉及我们软件范围内的数字,以及数字之间如何在语义上进行交互。
让我们回顾一下我们应该问的问题:
如果您...
检查代码文本中的独立清单常量基本值。慢慢而又沉思地问每个问题,以此类推。考虑您的回答的强度。很多时候,答案不是黑白的,而是有一些被误解的含义和目的,学习的速度和理解的速度。还需要查看它如何连接到它周围的软件机器。
最后,替换的答案是回答(在您的脑海中)建立连接的读者的强项或弱项(例如“获得”)的量度。他们越了解含义和目的,您就越“神奇”。
结论:仅当魔术足够大而难以检测出由混淆引起的错误时,才用符号常量替换基本值。
幻数是文件格式或协议交换开始处的字符序列。该号码用作健全性检查。
示例:打开任何GIF文件,您将在开始时看到:GIF89。“ GIF89”是魔术数字。
其他程序可以读取文件的前几个字符并正确识别GIF。
危险是随机二进制数据可能包含这些相同的字符。但这是不太可能的。
对于协议交换,您可以使用它来快速识别正在传递给您的当前“消息”是否已损坏或无效。
幻数仍然有用。
在编程中,“幻数”是一个应赋予符号名称的值,但通常作为一个字面量(通常在多个位置)插入到代码中。
出于同样的原因,这很不好,因为SPOT(单点真值)很不错:如果以后要更改此常量,则必须遍历代码以查找每个实例。这也是不好的,因为其他程序员可能不清楚这个数字代表什么,因此是“魔术”。
人们有时通过将这些常量移动到单独的文件中作为配置来进一步消除幻数。有时这是有帮助的,但也可能带来超出其价值的复杂性。
(foo[i]+foo[i+1]+foo[i+2]+1)/3
求值速度可能比循环快得多。如果要在3
不将代码重写为循环的情况下替换,则看到ITEMS_TO_AVERAGE
定义为的人3
可能会认为他们可以将其更改5
为代码平均包含更多项。相比之下,用字面量查看表达式的人3
会意识到,3
表示要加总的项数。
使用幻数未曾提及的问题...
如果你有非常多的人,赔率是相当不错的,你有两个不同的目的,其中,您使用幻数,值恰好是相同的。
然后,果然,您只需要出于一个目的就更改值。
我认为这是对我对您先前问题的回答的回应。在编程中,幻数是一个嵌入的数字常量,没有任何解释。如果它出现在两个不同的位置,则可能导致一个实例被更改而不是另一个实例被更改的情况。由于这两个原因,重要的是在使用它们的地方之外隔离和定义数字常量。
如何在类顶部使用默认值初始化变量?例如:
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
在方法顶部声明的变量更具可读性,恕我直言。
@ eed3si9n:我什至建议'1'是一个魔术数字。:-)
与幻数相关的一个原则是,您的代码处理的每个事实都应被声明为准确一次。如果您在代码中使用幻数(例如@marcio给出的密码长度示例),则很容易最终复制该事实,并且当您了解该事实更改时,便遇到了维护问题。
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
那么返回变量呢?
我特别发现在实现存储过程时具有挑战性。
想象下一个存储过程(我知道错误的语法,只是为了显示一个示例):
int procGetIdCompanyByName(string companyName);
如果它存在于特定表中,则返回公司的ID。否则,它返回-1。不知何故,这是一个神奇的数字。到目前为止,我读过的一些建议说,我确实必须做这样的设计:
int procGetIdCompanyByName(string companyName, bool existsCompany);
顺便说一句,如果公司不存在,应该返回什么?好的:它将将existesCompany设置为false,但也将返回-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();
}
}
}
const myNum = 22; const number = myNum / 11;
现在我的11岁可能是人或啤酒瓶之类的东西,所以我将11改为常数例如居民。