为什么到底是邪恶?


141

我知道Lisp和Scheme程序员通常会说eval除非绝对必要,否则应避免这样做。对于几种编程语言,我已经看到了相同的建议,但是尚未看到反对使用的明确参数列表eval。在哪里可以找到有关使用潜在问题的说明eval

例如,我知道GOTO过程编程中的问题(使程序难以阅读且难以维护,很难找到安全性问题,等等),但是我从未见过反对的论点eval

有趣的是,相同的论点GOTO应该对延续有效,但我看到例如Schemers不会说延续是“邪恶的”-使用它们时应该小心。与eval使用延续的代码相比,他们更不喜欢使用代码(据我所知-我可能是错的)。


5
评估不是邪恶的,但邪恶是评估的
Anurag

9
@yar-我认为您的评论表示一个非常以单一派遣对象为中心的世界观。它可能对大多数语言都有效,但是在Common Lisp中是不同的,Common Lisp中的方法不属于类,而在Clojure中则更是如此,在Clojure中,仅通过Java interop函数支持类。Jay将此问题标记为Scheme,它没有任何内置的类或方法的概念(可以将各种形式的OO作为库使用)。
Zak

3
@Zak,您是对的,我只知道我所知道的语言,但是即使您在不使用样式的情况下使用Word文档,也不会感到干燥。我的意思是使用该技术不要重复自己。OO不是通用的,是真的……
Dan Rosenstark 2010年

4
我乐于在这个问题上添加clojure标签,因为我相信Clojure用户可能会受益于此处发布的出色答案。
米哈尔Marczyk

...那么,对于Clojure,至少还有一个其他原因:您与ClojureScript及其衍生版本失去兼容性。
Charles Duffy 2014年

Answers:


148

人们不应该使用的原因有很多EVAL

初学者的主要原因是:您不需要它。

示例(假设Common Lisp):

用不同的运算符评估表达式:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (eval (list op 1 2 3)))))

最好这样写:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (funcall op 1 2 3))))

在很多例子中,初学者学习Lisp认为他们需要EVAL,但他们不需要-因为表达式是求值的,而且人们也可以求值函数的一部分。大多数情况下,使用EVALshow表示对评估人员缺乏了解。

宏也是同样的问题。初学者通常会在其中编写函数的地方编写宏-不了解宏的真正用途并且不了解函数已经完成了工作。

它通常是该工作使用的错误工具,EVAL并且通常表明初学者不了解通常的Lisp评估规则。

如果您认为需要EVAL,请检查是否有类似的东西FUNCALLREDUCE或者APPLY可以代替使用。

  • FUNCALL -使用参数调用函数: (funcall '+ 1 2 3)
  • REDUCE -在值列表上调用函数并合并结果: (reduce '+ '(1 2 3))
  • APPLY-调用以列表为参数的函数:(apply '+ '(1 2 3))

问:我真的需要评估吗,还是编译器/评估器已经是我真正想要的?

避免EVAL使用高级用户的主要原因:

  • 您想确保代码已编译,因为编译器可以检查代码中的许多问题并生成更快的代码,有时可以更快,更快速(这是因子1000 ;-))。

  • 构造并需要评估的代码无法尽早编译。

  • 评估任意用户输入会带来安全问题

  • 评估的某些使用EVAL可能会在错误的时间发生并造成构建问题

用简化的示例解释最后一点:

(defmacro foo (a b)
  (list (if (eql a 3) 'sin 'cos) b))

因此,我可能想编写一个基于第一个参数使用SIN或的宏COS

(foo 3 4)(sin 4)(foo 1 4)(cos 4)

现在我们可能有:

(foo (+ 2 1) 4)

这不会产生期望的结果。

然后可能要FOO通过EVALuating变量来修复宏:

(defmacro foo (a b)
  (list (if (eql (eval a) 3) 'sin 'cos) b))

(foo (+ 2 1) 4)

但这仍然不起作用:

(defun bar (a b)
  (foo a b))

只是在编译时不知道变量的值。

要避免的一般重要原因EVAL它通常用于丑陋的骇客。


3
谢谢!我只是不明白最后一点(在错误的时间进行评估?)-请您详细说明一下?
杰伊,2010年

41
+1,因为这是真正的答案-人们eval之所以回过头来,仅仅是因为他们不知道有特定的语言或库功能可以做他们想做的事情。JS的类似示例:我想使用动态名称从对象获取属性,所以我写:eval("obj.+" + propName)本来可以写obj[propName]
Daniel Earwicker 2010年

我明白你的意思了,Rainer!谢谢!
杰伊,2010年

@Daniel :"obj.+"?最后我检查了一下,+在JS中使用点引用时无效。
Hello71 2011年

2
@Daniel可能表示eval(“ obj。” + propName)应该可以正常工作。
claj 2011年

41

eval(用任何语言)都不是邪恶的,就像电锯不是邪恶的一样。这是一个工具。它碰巧是一个功能强大的工具,一旦被滥用,它就可以切断肢体并使其内脏(隐喻地讲),但是对于程序员工具箱中的许多工具,可以说是相同的,包括:

  • goto 和朋友
  • 基于锁的线程
  • 延续
  • 宏(hygenic或其他)
  • 指针
  • 可重启异常
  • 自修改代码
  • ...以及成千上万的演员

如果您发现自己必须使用任何这些功能强大,潜在危险的工具,请问自己三遍“为什么?” 一连串。例如:

“为什么我必须使用eval?” “因为foo。” “为什么foo是必需的?” “因为……”

如果您到达该链的末端,并且该工具仍然看起来像是正确的事情,那就去做。记录下地狱。从中测试地狱。一遍又一遍地仔细检查正确性和安全性。但是做吧。


谢谢-这就是我之前听说过eval的内容(“问问为什么”),但是我从未听说过或读过潜在的问题。我现在从这里的答案中看到它们是什么(安全和性能问题)。
杰伊,2010年

8
和代码的可读性。Eval可以完全破坏代码流,并使代码变得难以理解。
我的正确观点

我不明白为什么“基于锁的线程” [sic]在您的列表中。并发形式不涉及锁,并且锁的问题通常是众所周知的,但是我从未听说过有人将锁描述为“邪恶的”。
asveikau 2010年

4
asveikau:众所周知,基于锁的线程很难正确处理(我猜使用锁的生产代码中99.44%是不好的)。它不组成。容易将您的“多线程”代码转换为串行代码。(对此进行更正只会使代码变慢并且变得ated肿。)基于锁的线程有很多不错的选择,例如STM或actor模型,这使除最低级别代码之外的任何事情都无法使用它。
我的正确观点

“为什么连锁” :)请务必在3个步骤后停止操作,以免造成伤害。
szymanowski

27

只要您确切知道它将要发生什么,就可以使用Eval 。任何输入到其中的用户输入都必须经过检查和验证。如果您不知道如何百分百确定,那就不要这样做。

基本上,用户可以输入所讨论语言的任何代码,并且它将执行。您可以自己想象他会造成多大的伤害。


1
因此,如果我实际上是在使用不会直接复制用户输入的算法根据用户输入生成 S表达式,并且在某些特定情况下比使用宏或其他技术更轻松,更清楚的话,那么我认为没有“邪恶” “ 关于它?换句话说,eval的唯一问题与SQL查询和其他直接使用用户输入的技术相同吗?
杰伊,2010年

10
之所以称为“邪恶”,是因为做错事比做其他错事要糟得多。而且我们知道,新手会做错事。
Tor Valamo

3
我不会说在所有情况下都必须先验证代码才能逃避。例如,当实现一个简单的REPL时,您可能只是将输入未经检查地输入到eval中,这将不是问题(当然,在编写基于Web的REPL时,您需要一个沙箱,但对于正常情况并非如此)在用户系统上运行的CLI-REPL。
sepp2k 2010年

1
就像我说的那样,您必须确切地知道在向评估中输入内容时会发生什么。如果这意味着“它将在沙箱的限制内执行某些命令”,那就意味着它。;)
Tor Valamo 2010年

@TorValamo听说过越狱吗?
卢瓦克福雷-拉克鲁瓦

21

“我应该何时使用eval?” 可能是一个更好的问题。

简短的答案是“当您的程序打算在运行时编写另一个程序,然后运行它时”。遗传程序设计就是这种情况的一个示例,在这种情况下,使用它可能很有意义eval


14

IMO,这个问题不是LISP特有的。这是针对PHP的同一问题的答案,它适用于LISP,Ruby和其他具有eval的语言:

eval()的主要问题是:

  • 潜在的不安全输入。传递不受信任的参数是一种失败的方法。确保参数(或其一部分)被完全信任通常不是一件容易的事。
  • 棘手。使用eval()使代码更聪明,因此更难遵循。用布莱恩·科尼根(Brian Kernighan)的话说:“ 调试是一开始编写代码的两倍。因此,如果您尽可能聪明地编写代码,就定义而言,您不够聪明,无法调试它

实际使用eval()的主要问题只有一个:

  • 没有经验的开发人员在没有足够考虑的情况下使用它。

这里取

我认为这很棘手。对代码高尔夫和简洁代码的痴迷总是导致“聪明”代码(对于评估来说,评估代码是一个很好的工具)。但是,您应该编写代码以提高可读性,IMO,而不是为了证明自己很聪明并且不节省纸张(无论如何也不会打印出来)。

然后在LISP中,存在一个与运行eval的上下文有关的问题,因此不受信任的代码可以访问更多内容;无论如何,这个问题似乎很普遍。


3
EVAL的“错误输入”问题仅影响非Lisp语言,因为在这些语言中,eval()通常采用字符串参数,并且通常将用户的输入进行拼接。用户可以在其输入中包含引号,然后转义为生成的代码。但是在Lisp中,EVAL的参数不是字符串,除非绝对鲁ck(除非您用READ-FROM-STRING解析输入以创建S表达式,然后将其包含在其中),否则用户输入无法转入代码中。 EVAL代码而不引用它。如果您确实引用它,则无法转义引用)。
扔掉帐户


11

规范的答案是远离。我发现这很奇怪,因为它是一个原语,并且在七个原语(其他分别是cons,car,cdr,if,eq和quote)中,它得到的使用和喜爱最少。

From On Lisp:“通常,明确地打电话给eval就像在机场礼品店里买东西。等到最后一刻,您必须为有限的二流商品选择付出高昂的代价。”

那么我什么时候使用eval?一种正常的用法是通过评估将REPL包含在REPL中(loop (print (eval (read))))。每个人都很好用。

但是,您还可以根据宏定义函数,这些宏将编译通过将eval与反引号结合使用来评估。你走

(eval `(macro ,arg0 ,arg1 ,arg2))))

它将为您杀死上下文。

Swank(用于emacs粘液)充满了这些情况。他们看起来像这样:

(defun toggle-trace-aux (fspec &rest args)
  (cond ((member fspec (eval '(trace)) :test #'equal)
         (eval `(untrace ,fspec))
         (format nil "~S is now untraced." fspec))
        (t
         (eval `(trace ,@(if args `(:encapsulate nil) (list)) ,fspec ,@args))
         (format nil "~S is now traced." fspec))))

我认为这不是肮脏的骇客。我一直在使用它来将宏重新集成到函数中。


1
您可能需要检查内核语言;)
artemonster

7

关于Lisp eval的另外几点:

  • 它在全球环境下进行评估,从而失去了本地环境。
  • 有时,当您确实打算使用读取宏“#”时,您可能会想使用eval。在读取时进行评估。

我知道,Common Lisp和Scheme都可以使用global env。Clojure也是如此吗?
杰伊,2010年

2
在Scheme(至少对于R7RS,也许对于R6RS)中,必须将环境传递给eval。
csl

4

就像GOTO的“规则”一样:如果您不知道自己在做什么,就可能会一团糟。

除了仅从已知且安全的数据中构建内容外,还有一些语言/实现无法充分优化代码的问题。您最终可能会在其中包含解释后的代码eval


该规则与GOTO有什么关系?在任何编程语言中,有没有不能弄乱的功能?

2
@肯:没有GOTO规则,因此答案中的引号是。对于那些害怕自己思考的人们来说,这只是一个教条。评估也一样。我记得使用eval可以大大加快一些Perl脚本的速度。这是您工具箱中的一种工具。当其他语言结构更容易/更好时,新手经常使用eval。但是,完全避免这样做只是为了冷静并请教条主义的人?
stesch 2010年

4

评估只是不安全的。例如,您有以下代码:

eval('
hello('.$_GET['user'].');
');

现在,用户进入您的网站并输入url http://example.com/file.php?user=); $ is_admin = true; echo(

那么结果代码将是:

hello();$is_admin=true;echo();

6
他说的是Lisp以为不是php
fmsf,2010年

4
@fmsf他专门谈论Lisp,但通常eval以任何具有Lisp的语言谈论。
Skilldrick

4
@fmsf-这实际上是一个与语言无关的问题。它甚至适用于静态编译语言,因为它们可以通过在运行时调用编译器来模拟评估。
Daniel Earwicker 2010年

1
在这种情况下,该语言是重复的。我在这里看到过很多类似的作品。
fmsf 2010年

9
PHP eval与Lisp eval不同。看,它对字符串起作用,URL中的漏洞利用取决于能否关闭文本括号并打开另一个括号。Lisp eval不易受此类问题的影响。如果对沙箱进行了适当的沙箱化,那么您可以评估来自网络的输入数据(并且结构很容易行走)。
卡兹(Kaz)

2

评估不是邪恶的。评估并不复杂。它是一个编译传递给它的列表的函数。在大多数其他语言中,编译任意代码将意味着学习该语言的AST,并在编译器内部进行深入研究以找出编译器API。简而言之,您只需调用eval。

什么时候应该使用它?每当您需要编译某些东西时,通常是一个在运行时接受,生成或修改任意代码的程序

您什么时候不应该使用它?所有其他情况。

为什么在不需要时不使用它?因为您将以不必要的复杂方式进行操作,这可能会导致可读性,性能和调试问题。

是的,但是如果我是初学者,我如何知道是否应该使用它?始终尝试通过功能来实现所需的功能。如果这样不起作用,请添加宏。如果仍然无法解决问题,请评估!

遵循这些规则,您将永远不会用eval做恶:)


0

我非常喜欢Zak的答案,他已经明白了本质:在编写新语言,脚本或语言修改时使用eval。他没有真正进一步解释,所以我举一个例子:

(eval (read-line))

在这个简单的Lisp程序中,提示用户输入,然后评估他们输入的内容。为此,如果编译了程序,则必须存在整个符号定义集,因为您不知道用户可能输入哪些功能,因此必须将它们全部包括在内。这意味着,如果您编译此简单程序,则生成的二进制文件将是巨大的。

原则上,由于这个原因,您甚至不能将其视为可编译的语句。通常,一旦使用eval,您将在解释环境中进行操作,并且代码将无法再编译。如果不使用eval,则可以像C程序一样编译Lisp或Scheme程序。因此,在确定使用eval之前,您需要确保自己想要并需要处于解释环境中。

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.