发布于 2026-01-06 0 阅读
0

别再挥舞数字魔杖了

别再挥舞数字魔杖了

37你根本不知道那个数字是什么意思,对吧?一个没有上下文或标签的数字只是一个随机值,它什么也告诉我们不了。想象一下,你路过一个广告牌,上面画着一个人在船上,文字却是一个巨大的数字89。我会感到好奇,但同时也会完全摸不着头脑。我们理所当然地会拒绝毫无意义的数字……

那么,为什么它们在源代码中如此频繁地出现呢?任何常量出现在表达式中,例如 `f(x)` 12,我们都称之为“魔法数字”。它们仿佛通过某种神秘力量凭空出现,充斥着我们的代码。我们不知道它们从何而来,它们的含义是什么,也不知道它们是如何出现的。

虽然我不会像这篇文章那样深入探讨面试技巧,但我的书里有一章专门讲解面试技巧,认为这是程序员必备的技能之一。像修复“魔法数字”这样简单的问题,就能给面试官留下深刻的印象。

魔法的一个例子

面试时,我会让候选人设计一副扑克牌。我允许他们简化扑克牌,使用顺序编号的牌,而不是花色和点数。在大多数代码中,我看到的循环都类似于这样:

for i in 0..52:
    cards.add( i )
Enter fullscreen mode Exit fullscreen mode

这个数字52本身并不代表任何信息。之所以出现这个数字,仅仅是因为我们在讨论纸牌游戏,而这位考生假设使用的是一副标准的扑克牌52。当然,有很多纸牌游戏并非只有52张牌。

我要求他们将牌分发给多名玩家。我也接受拆分牌组。我经常收到类似这样的代码:

for i in 0..26:
    player1.add( cards[i] )
for i in 26..52:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

乍一看,这并不能明显看出它是将牌组一分为二。我需要在脑海中重新构建一下。编写代码时很容易出错。如果换一种情况,你看到的是这样的代码:

for i in 0..27:
    player1.add( cards[i] )
for i in 28..51:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

代码是否考虑了包含/排除的结束条件?里面是不是有很多差一错误?这到底是什么51?它跟我所知的任何数字都不匹配,甚至跟扑克牌的数字也不一样。

神奇数字

这段代码中:

for i in 0...52:
Enter fullscreen mode Exit fullscreen mode

52被称为“魔法数字”。未来的程序员对它一无所知。这类数字本身并不包含任何关于其用途的信息。

删除它们很简单。给这些数字起个名字就行了。

num_cards_in_deck = 52
for i in 0..num_cards_in_deck:
Enter fullscreen mode Exit fullscreen mode

我们立即提高了这段代码的质量。读者现在知道这个数字的52含义。循环现在能够合乎逻辑地对牌组中的每张牌执行操作。

给数字命名会消除它的神秘感。这里我们是在代码旁边做的,但通常情况下,像这样的符号num_cards_in_deck最终会成为全局常量。它们是与上下文无关的事实。如果多段代码需要同一个值,那就创建一个常量并共享它。

另一种解决这个难题的方法是添加注释,比如# create a standard size deck在循环语句中添加注释。但千万别这么做!注释是最后的手段。它们没有结构,编译器无法强制执行,而且最终都会过时。使用正确的符号名称才是最佳选择——代码胜过文字。

有了新的常量,我们就可以修改第二个循环:

for i in 0..(num_cards_in_deck/2):
    player1.add( cards[i] )
for i in (num_cards_in_deck/2)..num_cards_in_deck:
    player2.add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

代码质量再次显著提升。这些循环现在有了意义;很容易看出我们将牌组视为两半。而且,由于我们让编译器进行除法运算,出错的可能性也大大降低。此外,即使牌的数量发生变化,这个循环仍然有效。

我之所以勉强使用这个例子,仅仅是因为它经常出现在面试中。这两个循环仍然很糟糕。从防御性编程的角度来看,你根本不应该使用常量值。你应该使用 ` len(cards)int` 和 `int` len(cards)/2。很多“魔法数字”都可以通过这种方式消除。与其将代码绑定到一个任意值,不如基于你已有的知识。在这个例子中,我们有一个cards包含所有必要信息的集合。

放下魔杖

并非所有常量都是神奇的数字。有一些特殊情况,特别是…… 1。给1一个数加上一个数,得到的是它的自然后继数。这样就不会造成任何混淆。还有其他一些特殊情况,但在没有看到具体代码的情况下,我不想列举普遍的例外情况。

即使像 1 这样的数字2看起来并不神奇,但仍然可以改进。在我给出的例子中,它显然是将某物分成两半,但并没有明确表达“一半”的含义。在num_players这个例子中,最好用更清晰的方式来表达。很多时候,即使数字的含义显而易见,给它起个名字也很有帮助。名字能让代码更清晰易懂。

尽管魔法很有趣,但我们应该放下这种欲望,停止将这些数字输入到我们的代码中。


阅读我的书《什么是编程?》,了解成为一名优秀程序员需要具备哪些素质。我将目光投向人——软件存在的理由,软件的核心代码,以及你——键盘背后的那个人。


附录:更完善的交易循环

我认为上面展示的发牌循环仍然存在问题。虽然这与魔法数字无关,但以下代码以更简洁的方式处理玩家之间的发牌,避免了重复循环。

for i in len(cards):
    player_cards[i % num_players].add( cards[i] )
Enter fullscreen mode Exit fullscreen mode

另一种方案更接近于模拟发牌过程,并且完全避免了范围问题。它还使用了一个包含卡牌的面向对象玩家。这种代码适用于需要以可视化方式建模所有状态,或者涉及部分发牌的情况。它会保留中间状态。

cur_player = 0
while len(cards) != 0:
    player[cur_player].cards.add( cards.pop() )
    cur_player = (cur_player + 1) % num_players
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/mortoray/stop-waaving-the-wand-of-magic-numbers-3b1n