为什么eval邪恶呢?

时间:2010-04-03 13:50:57

标签: clojure scheme lisp common-lisp eval

我知道Lisp和Scheme程序员通常会说除非必要,否则应该避免使用eval。我已经看到了几种编程语言的相同建议,但我还没有看到反对使用eval的明确论据列表。我在哪里可以找到使用eval的潜在问题的帐户?

例如,我知道程序编程中GOTO的问题(使程序难以理解且难以维护,难以找到安全问题等),但我从未见过针对{{1 }}

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

12 个答案:

答案 0 :(得分:146)

为什么不应该使用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,但他们不需要它 - 因为表达式被评估,人们也可以评估函数部分。大多数情况下EVAL的使用表明对评估者缺乏了解。

宏是同样的问题。通常初学者会编写宏,他们应该编写函数 - 不了解宏是什么,而不是理解函数已经完成了这项工作。

这通常是使用EVAL作业的错误工具,它通常表明初学者不理解通常的Lisp评估规则。

如果您认为自己需要EVAL,请检查是否可以使用FUNCALLREDUCEAPPLY之类的内容。

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

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

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

  • 你想确保你的代码被编译,因为编译器可以检查代码中的许多问题并生成更快的代码,有时候很多(那是因子1000 ;-))更快的代码

    < / LI>
  • 构建且需要评估的代码无法尽早编译。

  • eval任意用户输入会打开安全问题

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

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

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

所以,我可能想要编写一个基于第一个参数的宏使用SINCOS

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

现在我们可能有:

(foo (+ 2 1) 4)

这不会产生预期的结果。

然后可能想要通过评估变量来修复宏FOO

(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的一般重要原因:它通常用于丑陋的黑客攻击。

答案 1 :(得分:41)

eval(用任何语言)都不像电锯那样邪恶。这是一个工具。它恰好是一个强大的工具,当被误用时,可以切断肢体和剔骨(隐喻地说),但程序员工具箱中的许多工具也可以这样说,包括:

  • goto和朋友
  • 基于锁定的线程
  • 延续
  • 宏(hygenic或其他)
  • 指针
  • restartable exceptions
  • 自我修改代码
  • ......和数千名演员。

如果你发现自己不得不使用任何这些功能强大的潜在危险工具,请问自己三次“为什么?”在一个链。例如:

  

“为什么我必须使用eval?”   “因为foo。” “为什么是foo   必要吗?“”因为...“

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

答案 2 :(得分:26)

Eval很好,只要你知道确实它会发生什么。必须检查和验证进入它的任何用户输入以及所有内容。如果您不知道如何100%确定,那就不要这样做。

基本上,用户可以输入相关语言的任何代码,并且它将执行。你可以想象自己可以做多少伤害。

答案 3 :(得分:21)

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

简短的回答是“当你的程序打算在运行时编写另一个程序,然后运行它”。 Genetic programming是使用eval可能有意义的情况示例。

答案 4 :(得分:14)

IMO,此问题并非针对LISP 。以下是针对PHP的相同问题的答案,它适用于LISP,Ruby和其他具有eval的语言:

  

eval()的主要问题是:

     
      
  • 潜在的不安全输入。传递不受信任的参数是一种方法   失败。这通常不是一项微不足道的任务   确保参数(或部分   它是完全可信的。
  •   
  • 棘手。使用eval()会使代码变得更聪明,因此更加困难   跟随。引用Brian Kernighan的话   “调试的难度是原来的两倍   首先编写代码。   因此,如果您将代码编写为   尽可能巧妙地,你是   定义,不够聪明,无法调试   它
  •   
     

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

     
      
  • 缺乏经验的开发人员在没有充分考虑的情况下使用它。
  •   

取自here

我认为棘手的一点是令人惊讶的。对代码高尔夫和简洁代码的痴迷总是产生“聪明”代码(对于这些代码来说,eval是一个很好的工具)。但是你应该编写你的代码以便于阅读,IMO,而不是为了证明你是一个聪明而且不是为了节省纸张(你无论如何都不打印它)。

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

答案 5 :(得分:12)

有很多很棒的答案,但这是另一个来自Racket的实施者Matthew Flatt:

http://blog.racket-lang.org/2011/10/on-eval-in-dynamic-languages-generally.html

他提出了许多已经涵盖的观点,但有些人可能会发现他的观点很有趣。

摘要:使用它的上下文会影响eval的结果,但程序员通常不会考虑它,从而导致意外的结果。

答案 6 :(得分:11)

规范的答案是远离。我觉得很奇怪,因为它是一个原始的,七个原始的(其他的是缺点,汽车,cdr,if,eq和引用),它远远地消耗了最少的使用和爱。

来自 On Lisp :“通常,明确地调用eval就像在机场礼品店买东西一样。等到最后一刻,你必须为有限的第二个选择支付高价 - 货物。“

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

但您也可以通过将eval与backquote相结合,根据将在编译之后评估的宏来定义函数。你去了

(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))))

我认为这不是一个肮脏的黑客。我自己一直用它来将宏重新集成到函数中。

答案 7 :(得分:7)

关于Lisp eval的另外几点:

  • 它在全球环境下进行评估,失去了当地的背景。
  • 有时你可能会想要使用eval,当你真的想要使用read-macro'#。在阅读时评估。

答案 8 :(得分:4)

就像GOTO“规则”:如果你不知道自己在做什么,你就会弄得一团糟。

除了仅使用已知和安全的数据构建内容之外,还存在一些语言/实现无法充分优化代码的问题。您最终可能会在eval内找到解释后的代码。

答案 9 :(得分:4)

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

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

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

然后生成的代码将是:

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

答案 10 :(得分:2)

Eval不是邪恶的。 Eval并不复杂。它是一个函数,用于编译传递给它的列表。在大多数其他语言中,编译任意代码意味着学习语言的AST并在编译器内部挖掘以找出编译器API。在lisp中,您只需调用eval。

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

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

为什么不在不需要的时候使用它?因为你会以不必要的复杂方式做某事,这可能会导致可读性,性能和调试方面的问题。

是的,但如果我是初学者,我怎么知道我是否应该使用它?始终尝试使用功能实现您需要的功能。如果这不起作用,请添加宏。如果仍然无效,那么eval!

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

答案 11 :(得分:0)

我非常喜欢Zak's answer并且他已经了解了问题的本质:当您编写新语言,脚本或语言修改时,会使用 eval 。他没有进一步解释,所以我举一个例子:

(eval (read-line))

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

原则上,由于这个原因,你甚至不能认为这是一个可编辑的陈述。通常,一旦使用 eval ,就会在解释环境中运行,并且无法再编译代码。如果你不使用 eval 那么就可以像C程序一样编译Lisp或Scheme程序。因此,在提交使用 eval 之前,您需要确保需要并且需要处于解释的环境中。