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

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

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

评论

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

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

这很有趣,我学会了憎恶...以后我可以用什么参数说服我的初中生...永无止境的故事我不会试图“说服”我宁愿让他们自己学习。根据您自己的经验,没有什么比上一堂课程/理想更重要的了。你想做的是灌输的。除非您想要一个Lemmings团队,否则不要这样做。

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

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

#1 楼


在可编译的语言中,不可在编译时检查魔术字符串的值。如果字符串必须匹配特定的模式,则必须运行程序以确保它适合该模式。如果使用了枚举之类的值,则该值至少在编译时有效,即使该值可能是错误的值。
如果在多个地方编写了魔术字符串,则必须全部更改没有任何安全性(例如编译时错误)。可以通过仅在一个位置声明它并重新使用该变量来解决。

错别字可能会成为严重的错误。如果您具有以下功能:

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


有人不小心键入:或更复杂的字符串,尤其是如果您的程序员不熟悉项目的本地语言。


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

这种实现是泄漏性的,需要外部文档或访问代码来理解应写的内容,尤其是因为它必须是完美的字符(如第3点中所述)。 >您可能会偶然在两个地方使用相同的魔术弦,但实际上它们是不同的,因此,如果您执行“查找并替换”并更改了两者,则其中一个可能会损坏,而另一个则会起作用。


评论


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

–阳谷
18-2-5在12:47

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

–埃德里克·铁罗斯(Erdrik Ironrose)
18-2-5在13:14

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

– KRyan
18年5月5日在16:25

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

– corsiKa
18年2月5日在16:47

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

–GrandOpener
18年2月6日在6:52

#2 楼

其他答案已经抓住的最高点不是“魔术值”是坏的,而是它们应该是:仅在其整个使用范围内一次(如果在体系结构上可行);
如果它们形成一组以某种方式相关的常量,则一起定义;
在它们所使用的应用程序中的适当通用级别上定义用过的;和
定义为限制它们在不适当的上下文中使用(例如,可以进行类型检查)。这些规则中的一个或多个。

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

我的结论是,过度使用常量(因此过多地使用值表示的假设或约束),即使它符合上述条件(但尤其是如果它们偏离了它们),可能意味着所设计的解决方案不够通用或结构合理(因此,我们不再真正在谈论常量的优缺点,而只是在谈论常量的优缺点。结构化代码)。

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

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

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

评论


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

–阿尔弗雷德·阿姆斯特朗(Alfred Armstrong)
18年2月5日在16:08

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

– WGroleau
18年2月5日在16:14

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

–马库斯
18年2月6日在13:54

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

–史蒂夫
18年2月6日在17:27

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

– corsiKa
18年2月7日在15:48

#3 楼


它们很难跟踪。
更改所有文件可能需要更改可能在多个项目中的多个文件(难以维护)。 。
不能重复使用。


评论


“不重用”是什么意思?

–再见
18-2-5在15:06

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

–杰森
18年5月5日在15:14

那么第2点和第4点相同吗?

–托马斯
18年2月6日在9:55

@ThomasMoors不,他是在谈论每次您要使用已经存在的魔术字符串时必须构建新字符串的方式,第二点是更改字符串本身

–皮埃尔·阿劳德
18-2-6在10:29



#4 楼

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

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


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

这可能会导致几个问题:


我需要参考外部文档以知道“ ProductName”甚至存在及其确切的拼写方式我需要参考该文档以查看该字段的数据类型。
执行此行代码之前,不会捕获此魔术字符串中的错别字。
当某人决定重命名该字段时,服务器(虽然很难防止数据丢失,但并非不可能),那么我无法轻松地搜索代码以了解应在何处调整此名称。 ,按实体类型进行组织。因此,现在我可以使用:

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


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


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

评论


+1带来了很多好处,不仅限于代码结构:IDE支持和工具,在大型项目中可以节省很多时间

– kmdreko
18年2月7日在19:36

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

– Lie Ryan
18-2-7在20:27

@LieRyan-生成纯常量并升级现有项目以使用它们要容易得多。也就是说,我正在生成静态类型,因此我可以精确地做到这一点

–汉斯·基辛
18年2月7日在20:43

#5 楼

魔术弦并不总是坏的,因此这可能就是您无法提出避免它们的全面理由的原因。 (通过“魔术字符串”,我假设您将字符串文字视为表达式的一部分,而不是定义为常量。)在某些特定情况下,应避免使用魔术字符串:


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

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

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


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

评论


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

–油菜籽
18-2-5在17:26



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

–雅克B
18-2-5在17:38



参考您的示例,我看到了将TOKEN_PLUS和TOKEN_MINUS替换为“ +”和“-”的switch语句。每次阅读它,我都会觉得它因此变得更难阅读和调试!绝对是我同意使用简单字符串更好的地方。

–Cort Ammon
18年2月5日在19:49

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

–克拉米
18年2月5日在22:02

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

–基督
18年2月7日在2:33

#6 楼

要添加到现有答案中:

国际化(i18n)

如果要在屏幕上显示的文本经过硬编码并隐藏在功能层中,则您将需要提供该文本到其他语言的翻译非常困难。

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

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

#7 楼

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

但是,这并不是很多开发人员在乎。