为什么是eval邪恶?
我知道Lisp和Scheme程序员通常会说,除非严格需要,否则应该避免使用eval。 我已经看到几种编程语言的相同的build议,但我还没有看到一个明确的反对使用eval的参数列表。 我在哪里可以find使用eval的潜在问题的帐户?
例如,我知道过程编程中的GOTO问题(使程序不可读,难以维护,难以find安全问题等),但是我从来没有看到反对eval的论点。
有趣的是,反对GOTO的同样的论点应该是有效的反对延续,但是我看到Schemers,例如,不会说延续是“邪恶的” – 你应该小心使用它们。 他们更有可能使用eval而不是使用代码使用代码(据我所知 – 我可能是错的)。
为什么不使用EVAL
有几个原因。
初学者的主要原因是:你不需要它。
示例(假设为Common Lisp):
使用不同的运算符评估expression式:
(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
,但他们不需要它 – 因为expression式被评估,并且也可以评估函数部分。 大多数情况下,使用EVAL
显示对评估者缺乏了解。
这与macros是一样的问题。 通常初学者编写macros,在那里写函数 – 不理解macros是为了什么,不理解函数已经完成了这个工作。
对于使用EVAL
的工作来说,这往往是错误的工具,它经常表明初学者不理解通常的Lisp评估规则。
如果您认为您需要EVAL
,那么请检查是否可以使用FUNCALL
, REDUCE
或APPLY
。
-
FUNCALL
– 用参数调用函数:(funcall '+ 1 2 3)
-
REDUCE
– 在值列表上调用函数并合并结果:(reduce '+ '(1 2 3))
-
APPLY
– 用一个列表作为参数调用一个函数:(apply '+ '(1 2 3))
。
问:我真的需要eval还是编译器/评估器已经是我真正想要的?
避免稍微高级用户的EVAL
主要原因是:
-
你要确保你的代码是编译的,因为编译器可以检查代码中的许多问题,并生成更快的代码,有时甚至更多(这是因素1000 ;-))更快的代码
-
那么构build和需要评估的代码就不能尽早编译。
-
评估任意用户input会导致安全问题
-
有些使用
EVAL
进行评估可能会在错误的时间发生并产生构build问题
用一个简单的例子来解释最后一点:
(defmacro foo (ab) (list (if (eql a 3) 'sin 'cos) b))
所以,我可能想写一个基于第一个参数的macros使用SIN
或COS
。
(foo 3 4)
确实(sin 4)
和(foo 1 4)
确实(cos 4)
。
现在我们可能有:
(foo (+ 2 1) 4)
这并没有给出预期的结果。
然后可以通过EVALuatingvariables来修复macrosFOO
:
(defmacro foo (ab) (list (if (eql (eval a) 3) 'sin 'cos) b)) (foo (+ 2 1) 4)
但是这仍然不起作用:
(defun bar (ab) (foo ab))
variables的值在编译时只是未知的。
避免EVAL
一般重要原因:它经常用于丑陋的黑客。
eval
(任何语言)都不是邪恶的,就像电锯不是邪恶的一样。 这是一个工具。 它恰好是一个强大的工具,当被滥用时,可以切断肢体和剔骨(隐喻地说),但是对于程序员的工具箱中的许多工具也是如此:
-
goto
和朋友 - 基于锁的线程
- 延续
- macros(hygenic或其他)
- 指针
- 可重启的exception
- 自修改代码
- …和数千人的阵容。
如果你发现自己不得不使用任何这些强大的,有潜在危险的工具,问自己三次“为什么? 在一个链。 例如:
“为什么我必须使用
eval
?” “因为富。” “为什么有必要?” “因为……”
如果你到了那个链条的末端,这个工具看起来仍然是正确的,那就去做吧。 记下地狱。 testing它的地狱。 仔细检查一遍又一遍的正确性和安全性。 但是这样做。
Eval是好的,只要你确切地知道什么是进入它。 任何进入它的用户input都必须经过检查和确认。 如果你不知道如何100%确定,那就不要这样做。
基本上,用户可以键入任何有关该语言的代码,并执行。 你可以想象自己可以做多less伤害。
“我应该什么时候使用eval
?” 可能是一个更好的问题。
简短的答案是“当你的程序打算在运行时写另一个程序,然后运行它”。 遗传编程是一个使用eval
很有意义的例子。
国际海事组织, 这个问题不是特定于LISP的 。 这里是关于PHP的同一个问题的答案,它适用于LISP,Ruby和其他具有eval的语言:
eval()的主要问题是:
- 潜在的不安全的input。 传递一个不可信的参数是一种失败的方法。 确保一个参数(或其一部分)是完全可信的往往不是一件容易的事情。
- Trickyness。 使用eval()可以使代码更加灵活,因此更难以遵循。 引用Brian Kernighan“ debugging比编写代码要困难一倍,因此,如果您尽可能巧妙地编写代码,那么根据定义,您的debugging不够聪明 ”
实际使用eval()的主要问题只有一个:
- 缺乏经验的开发者没有足够的考虑就使用它。
从这里采取。
我觉得棘手的一块是一个惊人的观点。 对代码高尔夫和简洁的代码的痴迷总是导致“聪明”的代码(为此,evals是一个很好的工具)。 但是,为了便于阅读,你应该编写你的代码,而不是为了certificate你是聪明人,而不是为了节省纸张 (反正你不会打印它)。
然后在LISP中,有一些与运行eval的上下文有关的问题,所以不可信的代码可以访问更多的东西; 无论如何,这个问题似乎是常见的。
有很多很好的答案,但这里还有另外一个来自球拍实施者Matthew Flatt的观点:
http://blog.racket-lang.org/2011/10/on-eval-in-dynamic-languages-generally.html
他提出了许多已经涵盖的观点,但有些人可能会发现他的观点很有趣。
简介:使用它的上下文会影响eval的结果,但程序员经常不会考虑这个结果,从而导致意外的结果。
规范的答案是远离。 我觉得这很奇怪,因为它是一个原始的,在七个原始的(其他的是cons,car,cdr,if,eq和quote)的情况下,它的用途和爱情远远不够。
来自Lisp :“通常情况下,明确地打电话给eval就像是在机场的礼品店里买东西,等到最后一刻,就要付出高昂的价格来购买有限的二手货。
那么我什么时候使用eval? 一个正常的使用是通过评估(loop (print (eval (read))))
在你的REPL中有一个REPL。 每个人都很好用。
但是,你也可以用macros来定义函数,这些macros将在编译之后通过将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))))
我不认为这是一个肮脏的黑客。 我一直使用它来将macros重新集成到函数中。
另外几个关于Lisp的评论:
- 它在全球环境下进行评估,失去了当地的环境。
- 有时你可能会试图使用eval,当你真的打算使用读macros“#”。 在阅读时进行评估。
就像GOTO的“规则”一样:如果你不知道自己在做什么,你可以弄得一团糟。
除了仅从已知和安全的数据中构build出一些东西之外,还有一些问题是某些语言/实现不能充分地优化代码。 你可能会在eval
内部解释代码。
Eval只是不安全的。 例如,您有以下代码:
eval(' hello('.$_GET['user'].'); ');
现在用户来到您的网站并inputurl http://example.com/file.php?user= ); $ is_admin = true; echo(
那么结果代码将是:
hello();$is_admin=true;echo();
Eval不是邪恶的。 评估并不复杂。 这是一个编译你传递给它的列表的函数。 在大多数其他语言中,编译任意代码将意味着学习语言的AST并在编译器内部进行挖掘以找出编译器API。 在lisp中,您只需调用eval。
你什么时候使用它? 无论何时你需要编译一些东西,通常是一个在运行时接受,生成或修改任意代码的程序 。
你什么时候不该用它? 所有其他情况。
为什么你不需要使用它? 因为你会以不必要的复杂方式做某些事情,这可能会导致可读性,性能和debugging方面的问题。
是的,但是如果我是初学者,我怎么知道我是否应该使用它? 总是尝试实现你所需要的function。 如果这不起作用,请添加macros。 如果这仍然不起作用,那么评估!
按照这些规则,你永远不会做邪恶与eval 🙂
我非常喜欢Zak的回答 ,他已经掌握了这个问题的实质:在编写新语言,脚本或修改语言时使用eval 。 他没有进一步解释,所以我举个例子:
(eval (read-line))
在这个简单的Lisp程序中,用户被提示input,然后他们input的内容被评估。 要编译这个程序, 整个符号定义必须存在,因为你不知道用户可以input哪些函数,所以你必须包含所有的符号定义。 这意味着如果你编译这个简单的程序,生成的二进制文件将是巨大的。
原则上,你甚至不能因为这个原因考虑这个可编辑的陈述。 一般来说,一旦你使用eval ,你在一个解释的环境中运行,代码不能再被编译。 如果你不使用eval,那么你可以像C程序一样编译一个Lisp或Scheme程序。 因此,在提交使用eval之前,您要确保您需要并且需要在解释的环境中。