魔术弦有什么问题?


164

作为经验丰富的软件开发人员,我学会了避免使用魔术字符串。

我的问题是,自从我使用它们已经有这么长时间了,我已经忘记了大多数原因。结果,我无法向经验不足的同事们解释为什么它们是一个问题。

有什么客观原因可以避免它们?它们会导致什么问题?


38
什么是魔力弦?和魔术数字一样吗?
Laiv

14
@Laiv:是的,它们类似于魔术数字。我喜欢deviq.com/magic-strings的定义:“魔术字符串是直接在应用程序代码中指定的字符串值,会对应用程序的行为产生影响。” (我根本没有想到en.wikipedia.org/wiki/Magic_string上的定义)
Kramii,

17
这很有趣,我学会了憎恶 ...以后我可以用什么参数说服我的初中生 ...永无止境的故事:-)。我不会试图“说服”我宁愿让他们自己学习。您所经历的课程/构想无非就是如此。你想做的是灌输的。除非您想要一个Lemmings团队,否则不要这样做。
Laiv

15
@Laiv:我很乐意让人们从自己的经验中学到东西,但是不幸的是,这对我来说不是一个选择。我在一家公立医院工作,那里的细微错误可能会损害患者的护理,而我们却负担不起可避免的维护费用。
Kramii '18年

6
@DavidArno,这正是他通过问这个问题所做的事情。
user56834 '02

Answers:


212
  1. 在可编译的语言中,不可在编译时检查魔术字符串的值。如果字符串必须匹配特定的模式,则必须运行程序以确保它适合该模式。如果使用了枚举之类的值,则该值至少在编译时有效,即使它可能是错误的值也是如此。

  2. 如果在多个位置编写了魔术字符串,则必须在没有任何安全性(例如编译时错误)的情况下更改所有字符串。可以通过仅在一个地方声明它并重新使用该变量来解决这个问题。

  3. 错别字可能会成为严重的错误。如果具有功能:

    func(string foo) {
        if (foo == "bar") {
            // do something
        }
    }
    

    有人不小心输入:

    func("barr");
    

    字符串越稀有或更复杂,就越糟,尤其是如果您有不熟悉项目本地语言的程序员。

  4. 魔术弦很少能自我记录。如果您看到一个字符串,那么该字符串将不会告诉您/应该是什么。您可能必须研究实现以确保您选择了正确的字符串。

    这种实现是泄漏性的,需要外部文档或访问代码来理解应写的内容,特别是因为它必须是完美的字符(如第3点所示)。

  5. IDE中缺少“查找字符串”功能,只有少数工具支持该模式。

  6. 您可能会偶然在两个地方使用相同的魔术弦,而实际上它们是不同的东西,因此,如果您执行“查找并替换”并更改了两者,则其中一个可能会损坏,而另一个则会工作。


34
关于第一个参数:TypeScript是一种可以对字符串文字进行类型检查的编译语言。这也会使参数2到4无效。因此,不是字符串本身就是问题,而是使用允许太多值的类型。可以将相同的推理应用于将魔术整数用于枚举。
Yogu,

11
由于我没有使用TypeScript的经验,因此我会根据您的判断去做。那我要说的是问题是未经检查的字符串(就像我使用的所有语言一样)。
Erdrik Ironrose

23
如果您更改期望的静态字符串文字类型,则@Yogu Typescript不会为您重命名所有字符串。您会得到编译时错误,以帮助您找到所有错误,但这只是对2的部分改进。并不是说它绝对令人赞叹(因为那是我所喜欢的功能),但绝对不会彻底消除枚举的优势。在我们的项目中,何时使用枚举以及何时不使用枚举仍然是我们不确定的一种开放式问题。两种方法都有烦恼和优势。
KRyan

30
我见过的一个大的字符串不像数字那样多,但是字符串可能会发生,就是当您有两个具有相同值的魔术值时。然后其中之一发生变化。现在,您正在遍历代码,将旧值更改为新值,这是单独工作的,但是您还要进行EXTRA工作,以确保您没有更改错误的值。使用常量变量,不仅不必手动进行操作,而且不必担心更改了​​错误的内容。
corsiKa

35
@Yogu我还要进一步指出,如果在编译时检查字符串文字的值,那么它将不再是魔术字符串。那时,它只是一个普通的const / enum值,碰巧是以一种有趣的方式编写的。从这个角度来看,我实际上会认为您的评论实际上支持了 Erdrik的观点,而不是反驳它们。
GrandOpener

89

其他答案已经抓住的最高点不是“魔术值”是坏的,而是它们应该是:

  1. 可识别地定义为常量;
  2. 在其整个使用范围内定义一次(如果在架构上可行);
  3. 如果它们形成一组以某种方式相关的常数,则一起定义;
  4. 在使用它们的应用程序中以适当的一般性级别进行定义;和
  5. 以限制它们在不适当上下文中使用的方式定义(例如,适合类型检查)。

通常将可接受的“常量”与“魔术值”区分开的是对这些规则中的一个或多个规则的某种违反。

很好地使用常量可以使我们表达代码的某些公理。

这使我得出最后一个结论,即过度使用常量(因此,过多地使用值表示的假设或约束),即使它符合上述标准(尤其是偏离标准的情况),可能意味着正在设计的解决方案不够通用或结构良好(因此,我们不再真正在谈论常量的利弊,而只是在讨论结构良好的代码的利弊)。

高级语言具有使用较低级语言的模式的构造,这些构造必须使用常量。相同的模式也可以在高级语言中使用,但不应使用。

但这可能是基于对所有情况的印象以及解决方案的模样的专家判断,而该判断的合理依据将在很大程度上取决于上下文。的确,就任何一般原则而言,这也许是没有道理的,只是断言“我已经年纪大了,已经看过这种工作,我熟悉,做得更好”!

编辑:接受了一项编辑,拒绝了另一项编辑,并且现在执行了我自己的编辑,现在我可以考虑一劳永逸地解决我的规则列表的格式和标点样式!


2
我喜欢这个答案。毕竟,“ struct”(以及其他所有保留字)是C编译器的魔力字符串。为它们编码的方法有好有坏。
Alfred Armstrong

6
例如,如果有人在您的代码中看到“ X:= 898755167 * Z”,则他们可能不知道它的意思,甚至更不可能知道它是错误的。但是,如果他们看到“ Speed_of_Light:常量整数:= 299792456”,则有人会查找它并建议正确的值(甚至可能是更好的数据类型)。
WGroleau

26
有些人完全错过了重点,而是写了COMMA =“,”而不是SEPARATOR =“,”。前者没有使任何事情变得更清晰,而后者陈述了预期的用法,并允许您稍后在一个地方更改分隔符。
marcus

1
@marcus,的确如此!当然,有一种情况是就地使用简单的文字值-例如,如果某个方法将一个值除以2,则简单地编写起来可能会更清晰,更简单value / 2,而不是value / VALUE_DIVISOR2其他地方定义后者。如果打算泛化处理CSV的方法,则可能希望将分隔符作为参数传递,而根本不定义为常量。但这只是上下文中的判断问题- SPEED_OF_LIGHT您想要明确命名@WGroleau的示例,但并非每个文字都需要此名称。
史蒂夫

4
如果需要说服魔术弦是“坏事”,则最佳答案要好于此答案。如果您知道并接受它们是“坏事”,并且需要找到最佳方法来满足它们以可维护的方式满足的需求,则此答案会更好。
corsiKa

34
  • 他们很难追踪。
  • 更改全部可能需要更改可能在多个项目中的多个文件(难以维护)。
  • 有时仅通过查看其价值就很难分辨其目的是什么。
  • 不可重复使用。

4
“不重用”是什么意思?
再见

7
与其创建一个变量/常量等并在所有项目/代码中重用它,不如在每个项目/代码中创建一个新的字符串,这将导致不必要的重复。
杰森

那么第2点和第4点相同吗?
托马斯

4
@ThomasMoors不,他是在谈论每次您想使用一个已经存在的魔术弦时必须构建新弦的方式,第二点是更改弦本身
Pierre Arlaud

25

实际示例:我正在使用第三方系统,其中“实体”与“字段”一起存储。基本上是EAV系统。由于添加另一个字段非常容易,因此您可以使用该字段的名称作为字符串来访问该字段:

Field nameField = myEntity.GetField("ProductName");

(注意魔术字符串“ ProductName”)

这可能会导致几个问题:

  • 我需要参考外部文档以知道“ ProductName”甚至存在及其确切拼写
  • 另外,我需要参考该文档以查看该字段的数据类型是什么。
  • 在执行此行代码之前,不会捕获此魔术字符串中的拼写错误。
  • 当有人决定在服务器上重命名此字段时(在防止数据丢失的同时很难,但并非没有可能),那么我无法轻松地搜索代码以查看应在何处调整此名称。

因此,我的解决方案是为这些名称生成按实体类型组织的常量。所以现在我可以使用:

Field nameField = myEntity.GetField(Model.Product.ProductName);

它仍然是一个字符串常量,可以编译为完全相同的二进制文件,但是具有几个优点:

  • 键入“模型”后,IDE将仅显示可用的实体类型,因此我可以轻松选择“产品”。
  • 然后,我的IDE仅提供可用于这种类型的实体(也可以选择)的字段名。
  • 自动生成的文档显示此字段的含义以及用于存储其值的数据类型。
  • 从常量开始,我的IDE可以找到使用该确切常量的所有位置(与其值相对)
  • 打字错误将被编译器捕获。当使用新模型(可能在重命名或删除字段之后)重新生成常量时,这也适用。

在我的列表中的下一个:将这些常量隐藏在生成的强类型类后面-然后也保护数据类型。


+1会带来很多好处,而不仅限于代码结构:IDE支持和工具,在大型项目中可能是救命稻草
kmdreko

如果您的实体类型的某些部分足够静态,以至于为其实际定义一个常量名称是值得的,那么我认为仅为它定义一个适当的数据模型就更容易了nameField = myEntity.ProductName;
Lie Ryan

@LieRyan-生成纯常量并升级现有项目以使用它们要容易得多。这么说,我正在工作的生成静态类型的,所以我可以做的正是
汉斯·柯ST荷兰国际集团

9

魔术弦并不总是不好的,因此这可能是您无法提出避免它们的全面原因的原因。(通过“魔术字符串”,我假设您是将字符串文字表示为表达式的一部分,而不是定义为常量。)

在某些特定情况下,应避免使用魔术弦:

  • 同一字符串在代码中出现多次。这意味着您可能会有拼写错误的地方之一。这将是字符串更改的麻烦。将字符串变成一个常量,您将避免此问题。
  • 该字符串可能会更改,而与出现的代码无关。例如。如果字符串是文本显示给最终用户,则它很可能会独立于任何逻辑更改而更改。将这样的字符串分成一个单独的模块(或外部配置或数据库)将使更容易独立更改
  • 从上下文来看,字符串的含义并不明显。在这种情况下,引入常量将使代码更易于理解。

但是在某些情况下,“魔术弦”是可以的。假设您有一个简单的解析器:

switch (token.Text) {
  case "+":
    return a + b;
  case "-":
    return a - b;
  //etc.
}

这里确实没有魔术,并且上述问题均不适用。恕我直言,定义string Plus="+"等不会有任何好处。请保持简单。


7
我认为您对“魔力弦”的定义是不够的,它需要具有一些隐藏/模糊/神秘的概念。在该反例中,我不会将“ +”和“-”称为“ magic”,也比在零中将零称为魔术if (dx != 0) { grad = dy/dx; }
Rupe

2
@Rupe:我同意,但是OP使用定义“ 在应用程序代码中直接指定的字符串值,这些值会影响应用程序的行为。 ”它不需要字符串是神秘的,因此这是我在其中使用的定义答案。
JacquesB

7
参考你的榜样,我已经看到它取代了switch语句"+",并"-"TOKEN_PLUSTOKEN_MINUS。每次阅读它,我都会因此而感到难以阅读和调试!绝对是我同意使用简单字符串更好的地方。
Cort Ammon

2
我确实同意魔术字符串有时是合适的:避免使用它们是一个经验法则,所有经验法则都有例外。我们希望,当我们清楚为什么他们可以是一件坏事,我们将能够做出明智的选择,而不是做的事情,因为(1)我们从来没有了解,可以有更好的方法,或(2)我们资深开发人员或编码标准已告知操作不同的方法。
Kramii '18年

2
我不知道这里有什么“魔术”。在我看来,这些看起来像基本的字符串文字。
tchrist

6

要添加到现有答案中:

国际化(i18n)

如果要在屏幕上显示的文本是经过硬编码的并且埋藏在功能层中,那么将文本翻译成其他语言的时间将非常困难。

一些开发环境(例如Qt)通过从基本语言文本字符串到已翻译语言的查找来处理翻译。魔术字符串通常可以幸免于此-直到您决定要在其他地方使用相同的文本并输入错误为止。即使这样,当您要添加对另一种语言的支持时,也很难找到需要翻译的魔术字符串。

某些开发环境(例如MS Visual Studio)采用另一种方法,要求所有转换后的字符串都保存在资源数据库中,并通过该字符串的唯一ID读取当前语言环境。在这种情况下,带有魔术字符串的应用程序无法简单地将其翻译成另一种语言。高效的开发要求所有文本字符串都必须输入到资源数据库中,并在首次编写代码时具有唯一的ID,此后相对容易。事实发生后尝试回填通常需要付出很大的努力(是的,我去过那里!),因此首先做好事情要好得多。


3

这并不是每个人都优先考虑的事情,但是如果您希望能够以自动化方式在代码上计算耦合/内聚度量,那么魔术字符串几乎就不可能做到这一点。一个地方的字符串将引用另一个地方的类,方法或函数,并且没有简单,自动的方法仅通过解析代码来确定该字符串是否与类/方法/函数耦合。只有基础框架(例如Angular)才能确定存在链接-并且只能在运行时进行链接。为了自己获取耦合信息,解析器将必须了解您所使用的框架的所有知识,而不仅仅是编码语言。

但是,这并不是很多开发人员关心的事情。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.