Eval 到底为什么邪恶?

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

例如,我知道程序编程中 GOTO的问题(使程序不可读和难以维护,使安全问题难以发现等等) ,但我从未见过反对 eval的论点。

有趣的是,反对 GOTO的同样参数应该对延续有效,但是我看到,例如,Schemer 不会说延续是“邪恶的”——在使用它们时应该小心。他们更可能反对使用 eval的代码,而不是使用延续的代码(据我所知——我可能错了)。

16155 次浏览

Eval 很好,只要你知道 没错是什么就行。任何用户输入进入它必须检查和验证和一切。如果你不知道如何百分之百确定,那就别做。

基本上,用户可以为所涉及的语言键入任何代码,并执行该代码。你可以想象他会造成多大的伤害。

就像 GOTO 的“规则”: 如果你不知道你在做什么,你可能会搞得一团糟。

除了仅仅从已知的和安全的数据中构建一些东西之外,还有一个问题是,有些语言/实现不能对代码进行足够的优化。您可能会在 eval中得到解释代码。

伊瓦尔只是没有安全感。 例如,您有以下代码:

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

现在用户访问站点并输入 url http://example.com/file.php?user=) ; $is _ admin = true; echo (

然后得到的代码是:

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

eval(在任何语言中)都不是邪恶的,就像电锯不是邪恶的一样。这是个工具。它碰巧是一个强大的工具,如果使用不当,可以切断四肢和内脏(隐喻地说) ,但同样可以说在程序员的工具箱中的许多工具,包括:

  • goto和朋友
  • 基于锁的线程
  • 延续
  • 宏(卫生或其他)
  • 指示
  • 可重新启动的异常
  • 程序自修改
  • 成千上万的演员。

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

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

如果到了链的末端,工具看起来仍然是正确的事情,那么就去做。把它记录下来。好好试试。反复检查正确性和安全性。但还是照做吧。

不应该使用 EVAL的原因有几个。

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

示例(假设是 Common Lisp) :

使用不同的运算符对表达式进行 EVALuate:

(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 '+ 1 2 3)
  • REDUCE-对一列值调用一个函数,并将结果组合在一起: (reduce '+ '(1 2 3))
  • 以列表作为参数调用函数: (apply '+ '(1 2 3))

问: 我真的需要 eval 吗? 还是编译器/计算器已经是我真正需要的了?

对于稍微高级一些的用户,避免使用 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的一个普遍的重要原因是: 它经常用于丑陋的黑客攻击。

我的天,这个问题不是 LISP 特有的。这里有一个关于 PHP 的相同问题的答案,它适用于 LISP、 Ruby 和其他具有 eval 的语言:

Eval ()的主要问题是:

  • 潜在的不安全输入。 传递不受信任的参数是实现 这通常不是一个微不足道的任务 确保一个参数(或部分) 是完全可信的。
  • 使用 eval ()使代码更聪明,因此更困难 引用 Brian Kernighan 的话 ”< em > 调试的难度是 编写代码。 因此,如果将代码编写为 你尽可能地聪明 定义,没有足够的智能来调试 它

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

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

取自 给你

我认为这个棘手的问题是一个令人惊讶的观点。对代码高尔夫和简洁代码的痴迷总是导致“聪明”的代码(对于这些代码,评估是一个很好的工具)。但是你应该为了可读性而编写代码,IMO,而不是为了证明你是一个聪明人和 不是为了省纸(无论如何你都不会打印它)。

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

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

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

规范的答案是远离。我觉得很奇怪,因为它是一个原语,而且在七个原语(其他是 con,car,cdr,if,eq 和 quote)中,它得到的使用和爱是最少的。

来自 在 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))))

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

关于 Lisp eval 的另外几点:

  • 它在全局环境下进行评估,失去了本地上下文。
  • 有时您可能会想使用 eval,而实际上是想使用 read-Macro’# 在读取时计算。

有很多很好的答案,但是这里有另一个来自 Matthew Flatt 的观点,他是 Racket 的实施者之一:

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

他提出了许多已经被覆盖的观点,但一些人可能会发现他的看法仍然很有趣。

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

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

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

- 你什么时候不该用它?-所有其他案子。

为什么不能在不需要的时候用呢?因为您将以不必要的复杂方式执行某些操作,这可能会导致可读性、性能和调试方面的问题。

是的,但是如果我是一个初学者,我怎么知道我是否应该使用它?始终尝试用函数实现您需要的东西。如果这不起作用,添加宏。如果还是不行,那就 Eval!

遵守这些规则,你就永远不会对 eval 做坏事:)

我非常喜欢 Zak 的回答,他已经抓住了问题的实质: Eval是用来编写新语言、脚本或修改语言的。他没有进一步解释,所以我举个例子:

(eval (read-line))

在这个简单的 Lisp 程序中,提示用户输入,然后对他们输入的任何内容进行计算。为了实现这一点,在编译程序时,必须提供 完整的符号定义集,因为您不知道用户可能会输入哪些函数,所以必须包含所有函数。这意味着如果编译这个简单的程序,生成的二进制文件将是巨大的。

作为一个原则问题,由于这个原因,您甚至不能认为这是一个可编译的语句。通常,一旦使用 Eval,就是在解释环境中进行操作,不能再编译代码。如果您不使用 Eval,那么您可以像编译 C 程序一样编译 Lisp 或 Scheme 程序。因此,在决定使用 Eval之前,您需要确保您想要并且需要处于一个解释环境中。