关闭。这个问题是基于意见的。它目前不接受答案。想改进这个问题?更新问题,以便可以通过编辑这篇文章用事实和引用来回答它。 2年前关闭。改进这个问题
什么是幻数?
为什么要避免?
有合适的情况吗?
const myNum = 22; const number = myNum / 11;
现在我的 11 可能是人或啤酒瓶或其他东西,所以我会更改 11到一个常数,例如居民。
幻数是代码中数字的直接用法。
例如,如果您有(在 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 等静态分析工具会检测代码中幻数的使用并建议重构。
幻数是一个硬编码值,可能会在以后发生变化,但因此很难更新。
例如,假设您有一个在“您的订单”概览页面中显示最近 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
”——如果你在一个复杂的函数中遇到它,其他不熟悉代码的开发者可能会问自己“WTF is 50???”
这就是为什么最好在 1 个位置(“const int NumOrdersToDisplay = 50
”)中包含如此模糊和任意的数字,因为这会使代码更具可读性(“if a < NumOrdersToDisplay
”,这也意味着您只需要在 1 个明确定义的位置进行更改.
幻数适用的地方是通过标准定义的所有内容,即 SmtpClient.DefaultPort = 25
或 TCPPacketSize = whatever
(不确定这是否是标准化的)。此外,仅在 1 个函数中定义的所有内容可能都是可以接受的,但这取决于上下文。
SmtpClient.DefaultPort = 25
可能比SmtpClient.DefaultPort = DEFAULT_SMTP_PORT
更清晰更。
25
,并确保您只更改用于 SMTP 端口的 25
,而不是 25,例如宽度表格列或要在页面上显示的记录数。
IANA
分配的数字。
您是否查看过 magic number? 的 Wikipedia 条目
它详细介绍了幻数引用的所有方式。这是关于幻数作为一种糟糕的编程习惯的引用
幻数一词也指在源代码中直接使用数字而不作解释的不良编程习惯。在大多数情况下,这会使程序更难阅读、理解和维护。尽管大多数指南对数字 0 和 1 都有例外,但最好将代码中的所有其他数字定义为命名常量。
幻数与。符号常数:何时替换?
魔术:未知语义
符号常量 -> 提供正确的语义和正确的上下文以供使用
语义:事物的意义或目的。
“创建一个常量,根据含义命名,并用它替换数字。” ——马丁·福勒
首先,幻数不仅仅是数字。任何基本值都可以是“魔法”。基本值是清单实体,例如整数、实数、双精度、浮点数、日期、字符串、布尔值、字符等。问题不在于数据类型,而是出现在我们的代码文本中的值的“神奇”方面。
我们所说的“魔法”是什么意思?准确地说:通过“魔术”,我们打算在我们的代码上下文中指向值的语义(意义或目的);它是未知的、不可知的、不清楚的或令人困惑的。这就是“魔法”的概念。当一个基本值的语义或存在目的在没有特殊辅助词(例如符号常数)的情况下从周围的上下文中快速、容易地知道、清楚和理解(不混淆)时,它就不是魔法。
因此,我们通过测量代码阅读者从周围环境中了解、清晰和理解基本值的含义和目的的能力来识别幻数。越不为人所知、越不清晰、越迷茫,基本价值就越“神奇”。
有用的定义
迷惑:使(某人)感到困惑或困惑。
困惑:使(某人)变得困惑和困惑。
困惑的:完全困惑的;很不解。
迷惑:完全迷惑或困惑。
不解:无法理解;困惑。
理解:感知(单词、语言或说话者)的预期含义。
含义:一个词、文本、概念或动作的含义。
意思是:打算传达、指示或提及(特定事物或概念);表示。
表示:表示。
指示:指示某事的标志或信息。
指出:指出;节目。
符号:一个对象、性质或事件,其存在或发生表明其他事物可能存在或发生。
基本
我们的魔法基本价值观有两种情况。只有第二个对程序员和代码来说是最重要的:
一个单独的基本值(例如数字),其含义是未知的、不可知的、不清楚的或令人困惑的。上下文中的基本值(例如数字),但其含义仍然未知、不可知、不清楚或令人困惑。
“魔术”的一个总体依赖是单独的基本值(例如数字)如何没有众所周知的语义(如 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 -- 死
undead: INTEGER = -1 -- Min undead(几乎死了)
僵尸:INTEGER = -10 -- 最大不死生物(非常不死生物)
从上面的符号常数,我们开始对我们的 D&D 游戏中的怪物的活跃、死亡和“不死”(以及可能的后果或后果)有了一个心理图景。没有这些词(符号常数),我们只剩下从 -10 .. 10
范围内的数字。如果游戏的不同部分依赖于数字范围对各种操作(如 attack_elves
或 seek_magic_healing_potion
)的含义,那么没有单词的范围就会让我们处于一个可能非常混乱的地方,并且可能在我们的游戏中出现错误。
因此,在搜索和考虑替换“幻数”时,我们希望就软件上下文中的数字提出非常有目的性的问题,甚至是数字如何在语义上相互交互。
结论
让我们回顾一下我们应该问的问题:
你可能有一个神奇的数字,如果......
基本价值在您的软件世界中是否有特殊意义或目的?即使在适当的上下文中,特殊含义或目的是否可能是未知的、不可知的、不清楚的或令人困惑的?一个正确的基本价值是否可以在错误的环境中被不当使用并带来不良后果?是否可以在正确的上下文中正确使用不正确的基本价值并产生不良后果?基本价值是否与特定上下文中的其他基本价值有语义或目的关系?一个基本值是否可以存在于我们的代码中的多个位置,每个位置具有不同的语义,从而使我们的读者感到困惑?
检查代码文本中的独立清单常量基本值。慢慢地、深思熟虑地询问每一个关于这种价值的实例的问题。考虑你的答案的强度。很多时候,答案不是非黑即白,而是带有被误解的含义和目的、学习速度和理解速度的阴影。还需要看看它是如何连接到它周围的软件机器上的。
最后,替换的答案是回答读者建立联系的强弱(例如“得到它”)的衡量标准(在你的脑海中)。他们越快理解意义和目的,你的“魔法”就越少。
结论:只有当魔法大到足以导致难以检测到由混淆引起的错误时,才用符号常量替换基本值。
幻数是文件格式或协议交换开头的一系列字符。这个数字用作健全性检查。
示例:打开任何 GIF 文件,您会在最开始看到:GIF89。 “GIF89”是神奇的数字。
其他程序可以读取文件的前几个字符并正确识别 GIF。
危险在于随机二进制数据可能包含这些相同的字符。但这不太可能。
至于协议交换,您可以使用它来快速识别正在传递给您的当前“消息”已损坏或无效。
幻数仍然有用。
在编程中,“幻数”是一个应该被赋予符号名称的值,但它却作为文字滑入代码中,通常在多个地方。
这很糟糕,原因与 SPOT(Single Point of Truth)很好的原因相同:如果您想稍后更改此常量,则必须在代码中寻找每个实例。这也很糟糕,因为其他程序员可能不清楚这个数字代表什么,因此是“魔术”。
人们有时会进一步消除幻数,将这些常量移动到单独的文件中以充当配置。这有时会有所帮助,但也会造成比其价值更多的复杂性。
(foo[i]+foo[i+1]+foo[i+2]+1)/3
这样的表达式的计算速度可能比循环快得多。如果要替换 3
而无需将代码重写为循环,则将 ITEMS_TO_AVERAGE
定义为 3
的人可能会认为他们可以将其更改为 5
并让代码平均更多项。相比之下,使用文字 3
查看表达式的人会意识到 3
表示相加的项目数。
使用幻数没有提到的一个问题......
如果你有很多,那么很有可能你有两个不同的目的,你正在使用幻数,其中的值恰好是相同的。
然后,果然,你需要改变价值......只有一个目的。
幻数也可以是具有特殊硬编码语义的数字。例如,我曾经看到一个系统,其中记录 ID > 0 被正常处理,0 本身是“新记录”,-1 是“这是根”,-99 是“这是在根中创建的”。 0 和 -99 将导致 WebService 提供新 ID。
这样做的不利之处在于,您将空格(用于记录 ID 的带符号整数的空格)用于特殊能力。也许您永远不想创建 ID 为 0 或负 ID 的记录,但即使不是,每个查看代码或数据库的人都可能会偶然发现这一点并一开始感到困惑。不用说,这些特殊值没有得到很好的记录。
可以说,22, 7, -12 and 620 也可以算作幻数。 ;-)
我认为这是对我的 answer 对您之前的问题的回应。在编程中,幻数是一个嵌入的数字常数,无需解释即可出现。如果它出现在两个不同的位置,则可能导致一个实例被更改而不是另一个实例被更改的情况。出于这两个原因,在使用它们的地方之外隔离和定义数字常量很重要。
我一直以不同的方式使用术语“幻数”,作为存储在数据结构中的模糊值,可以作为快速有效性检查进行验证。例如 gzip 文件的前三个字节包含 0x1f8b08,Java 类文件以 0xcafebabe 开头,等等。
您经常会看到文件格式中嵌入了幻数,因为文件可以相当杂乱地发送,并且会丢失有关其创建方式的任何元数据。然而,幻数有时也用于内存数据结构,如 ioctl() 调用。
在处理文件或数据结构之前快速检查幻数可以让人们及早发出错误信号,而不是通过可能冗长的处理一路走来宣布输入是完整的胡说八道。
值得注意的是,有时您确实希望代码中有不可配置的“硬编码”数字。有许多 famous ones,包括 0x5F3759DF,用于优化的逆平方根算法。
在我发现需要使用此类幻数的极少数情况下,我在代码中将它们设置为 const,并记录使用它们的原因、它们的工作方式以及它们的来源。
用默认值初始化类顶部的变量怎么样?例如:
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);
顺便问一下,如果公司不存在,它应该返回什么?好的:它将existsCompany设置为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();
}
}
}
不定期副业成功案例分享
TRUE
/FALSE
)