【问题标题】:Why exactly is eval evil?为什么 eval 到底是邪恶的?
【发布时间】:2011-02-04 00:15:53
【问题描述】:

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

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

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

【问题讨论】:

  • eval 不是邪恶的,但邪恶是 eval 所做的
  • @yar - 我认为你的评论表明了一个非常单一的以对象为中心的世界观。它可能对大多数语言都有效,但在 Common Lisp 中会有所不同,其中方法不属于类,在 Clojure 中更不同,其中类仅通过 Java 互操作函数支持。 Jay 将此问题标记为 Scheme,它没有任何内置的类或方法概念(各种形式的 OO 可作为库使用)。
  • @Zak,你说得对,我只知道我知道的语言,但即使你使用 Word 文档而不使用样式,你也不会干。我的观点是使用该技术不要重复自己。 OO 不是通用的,真的……
  • 我冒昧地为这个问题添加了 clojure 标签,因为我相信 Clojure 用户可能会从这里发布的优秀答案中受益。
  • goto 是“邪恶的”,因为它是一种突变形式:实际上,一个新值被突然分配给指令指针。延续不涉及突变;纯函数式语言可以具有延续性。它们比 if 和 while 之类的控制结构更纯粹,尽管它们是 goto 和标签的轻语法糖,但 Dijkstra 没问题。

标签: clojure scheme lisp common-lisp eval


【解决方案1】:

不应该使用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,请检查是否可以使用FUNCALL、REDUCE 或APPLY 之类的东西。

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

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

对于稍微高级一点的用户避免使用EVAL 的主要原因:

  • 您要确保您的代码已编译,因为编译器可以检查代码是否存在许多问题并生成更快的代码,有时会产生 MUCH MUCH MUCH(即 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:

(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,因为这是真正的答案——人们依赖eval 仅仅是因为他们不知道有特定的语言或库功能可以做他们想做的事情。来自 JS 的类似示例:我想使用动态名称从对象中获取属性,所以我写:eval("obj.+" + propName),而我本来可以写 obj[propName]。
  • 我明白你的意思了,雷纳!谢谢!
  • @Daniel: "obj.+"?最后我检查了一下,+ 在 JS 中使用点引用时无效。
  • @Daniel 可能意味着 eval("obj." + propName) 应该按预期工作。
【解决方案2】:

eval(在任何语言中)并不邪恶,就像电锯不邪恶一样。它是一种工具。它恰好是一个强大的工具,当被滥用时,可以切断四肢和内脏(比喻地说),但程序员工具箱中的许多工具也可以这样说,包括:

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

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

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

如果您到达该链的末端,并且该工具看起来仍然是正确的做法,那么就去做吧。把它记录下来。测试一下它。一遍又一遍地检查正确性和安全性。但是去做吧。

【讨论】:

  • 谢谢——这就是我之前听说过的 eval (“问自己为什么”),但我还没有听说过或读过潜在的问题是什么。我现在从这里的答案中看到它们是什么(安全和性能问题)。
  • 和代码可读性。 eval 可以完全搞砸代码流并使其难以理解。
  • 我不明白为什么“基于锁的线程”[原文如此]在您的列表中。有一些并发形式不涉及锁,而锁的问题通常是众所周知的,但我从未听说有人将使用锁描述为“邪恶”。
  • asveikau:众所周知,基于锁的线程很难正确处理(我猜 99.44% 的生产代码使用锁是不好的)。它不作曲。它很容易将您的“多线程”代码转换为串行代码。 (对此进行纠正只会使代码变得缓慢和臃肿。)基于锁的线程有很好的替代方案,例如 STM 或 Actor 模型,它们可以在除最底层代码之外的任何东西中使用它。
  • “为什么链” :) 一定要在 3 步后停止它可能会受伤。
【解决方案3】:

Eval 没问题,只要您确切地知道其中的内容。任何进入其中的用户输入都必须经过检查和验证。如果您不知道如何 100% 确定,那就不要这样做。

基本上,用户可以为所讨论的语言键入任何代码,然后它就会执行。你可以想象他能造成多大的伤害。

【讨论】:

  • 因此,如果我实际上是在使用不会直接复制用户输入的算法基于用户输入生成 S-表达式,并且在某些特定的情况下更容易和更清晰情况比使用宏或其他技术,那么我想这没有什么“邪恶”的吗?换句话说,eval 的唯一问题与 SQL 查询和其他直接使用用户输入的技术相同吗?
  • 之所以被称为“邪恶”,是因为做错了比做错其他事情要糟糕得多。众所周知,新手会做错事。
  • 我不会说在所有情况下都必须先验证代码才能对其进行评估。例如,在实现一个简单的 REPL 时,您可能只是将输入提供给 eval unchecked,这不是问题(当然,在编写基于 Web 的 REPL 时,您需要一个沙箱,但通常情况并非如此在用户系统上运行的 CLI-REPL)。
  • 就像我说的,当你将你输入的内容输入到 eval 中时,你必须确切地知道会发生什么。如果这意味着“它将在沙箱的范围内执行一些命令”,那么这就是它的意思。 ;)
  • @TorValamo 听说过越狱吗?
【解决方案4】:

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

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

【讨论】:

  • 完美答案。
  • 在这种情况下,如果我们可以compile 然后funcall,为什么要eval?
【解决方案5】:

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

eval() 的主要问题是:

  • 潜在的不安全输入。传递不受信任的参数是一种 失败。这通常不是一项微不足道的任务 确保参数(或部分 其中) 完全受信任。
  • 诡计。 使用 eval() 使代码更聪明,因此更难 跟随。引用 Brian Kernighan "调试的难度是 首先编写代码。 因此,如果您将代码编写为 尽可能聪明地,你是,通过 定义,不够聪明,无法调试 它"

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

  • 缺乏经验的开发人员未经充分考虑就使用它。

取自here。

我认为棘手的部分是一个惊人的点。对代码高尔夫和简洁代码的痴迷总是导致“聪明”的代码(其中 eval 是一个很好的工具)。但是您应该编写代码以提高可读性,IMO,而不是为了证明您是个聪明人并且不是为了节省纸张(无论如何您都不会打印它)。

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

【讨论】:

  • EVAL 的“恶意输入”问题只影响非 Lisp 语言,因为在那些语言中,eval() 通常采用字符串参数,并且通常会拼接用户的输入。用户可以包括在他们的输入中引用并转义到生成的代码中。但是在 Lisp 中,EVAL 的参数不是字符串,除非您绝对鲁莽,否则用户输入无法转义到代码中(就像您使用 READ-FROM-STRING 解析输入以创建一个 S 表达式,然后将其包含在没有引用它的 EVAL 代码。如果你引用它,就无法逃避引用)。
【解决方案6】:

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

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

他提出了许多已经讨论过的观点,但有些人可能会觉得他的观点很有趣。

总结:使用它的上下文会影响 eval 的结果,但程序员往往不会考虑,从而导致意想不到的结果。

【讨论】:

    【解决方案7】:

    典型的答案是远离。我觉得很奇怪,因为它是一个原语,并且在七个原语中(其他是 cons、car、cdr、if、eq 和 quote),它的使用和喜爱最少。

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

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

    但您也可以通过将 eval 与反引号结合起来,以宏的形式定义函数,这些宏将在编译后进行评估。你走吧

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

    它会为你杀死上下文。

    Swank(对于 emacs slime)充满了这些情况。它们看起来像这样:

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

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

    【讨论】:

    • 您可能想查看内核语言 ;)
    【解决方案8】:

    关于 Lisp eval 的另外几点:

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

    【讨论】:

    • 我知道使用 global env 对于 Common Lisp 和 Scheme 都是正确的; Clojure 也是如此吗?
    • 在Scheme中(至少对于R7RS,也许对于R6RS也是如此)你必须将环境传递给eval。
    【解决方案9】:

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

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

    【讨论】:

    • 该规则与 GOTO 有什么关系?任何编程语言中是否有您不能搞砸的功能?
    • @Ken:没有 GOTO 规则,因此我的答案中使用了引号。对于那些害怕独立思考的人来说,这只是一种教条。与评估相同。我记得使用 eval 显着加快了一些 Perl 脚本的速度。它是您工具箱中的一种工具。当其他语言结构更容易/更好时,新手经常使用 eval 。但是完全避免它只是为了酷并取悦教条的人?
    【解决方案10】:

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

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

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

    那么生成的代码将是:

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

    【讨论】:

    • 他说的是 Lisp 而不是 php
    • @fmsf 他专门谈论的是 Lisp,但通常是关于 eval 的任何语言。
    • @fmsf - 这实际上是一个与语言无关的问题。它甚至适用于静态编译语言,因为它们可以通过在运行时调用编译器来模拟 eval。
    • 在这种情况下,语言是重复的。我在这里见过很多这样的。
    • PHP eval 不像 Lisp eval。看,它对字符串进行操作,而 URL 中的漏洞利用取决于能够关闭一个文本括号并打开另一个括号。 Lisp eval 对这种东西不敏感。您可以评估来自网络的输入数据,前提是您正确地对其进行沙箱处理(并且该结构很容易步行以执行此操作)。
    【解决方案11】:

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

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

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

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

    是的,但是如果我是初学者,我怎么知道我是否应该使用它?总是尝试用函数来实现你需要的东西。如果这不起作用,请添加宏。如果还是不行,那就评估吧!

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

    【讨论】:

      【解决方案12】:

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

      (eval (read-line))
      

      在这个简单的 Lisp 程序中,系统会提示用户输入,然后评估他们输入的任何内容。如果程序被编译,那么整个符号定义集必须存在,因为你不知道用户可能输入哪些函数,所以你必须将它们全部包含在内。这意味着如果你编译这个简单的程序,生成的二进制文件将是巨大的。

      原则上,出于这个原因,您甚至不能认为这是一个可编译的语句。一般来说,一旦你使用了eval,你就是在一个解释的环境中操作,代码就不能再被编译了。如果您不使用 eval,那么您可以像编译 C 程序一样编译 Lisp 或 Scheme 程序。因此,在承诺使用 eval 之前,您需要确保自己想要并且需要处于解释环境中。

      【讨论】:

        猜你喜欢
        • 2013-08-18
        • 2010-10-31
        • 2011-04-28
        • 2010-09-16
        • 2010-10-13
        • 2010-11-22
        • 2012-10-20
        相关资源
        最近更新 更多