Почему именно зло?
Я знаю, что программисты на Лиспе и Схеме обычно говорят, что eval
следует избегать, если это строго не необходимо. Я видел одну и ту же рекомендацию для нескольких языков программирования, но я еще не видел список четких аргументов против использования eval
, Где я могу найти информацию о потенциальных проблемах использования eval
?
Например, я знаю проблемы GOTO
в процедурном программировании (делает программы нечитаемыми и сложными в обслуживании, затрудняет поиск проблем безопасности и т. д.), но я никогда не видел аргументов против eval
,
Интересно, что те же аргументы против GOTO
должен быть действителен против продолжений, но я вижу, что, например, Schemers не скажет, что продолжения являются "злыми" - вы должны быть осторожны при их использовании. Они с гораздо большей вероятностью будут недовольны использованием кода eval
чем в коде, использующем продолжения (насколько я вижу - я могу ошибаться).
12 ответов
Есть несколько причин, почему не следует использовать 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))))
Есть много примеров, когда начинающие изучающие Лисп думают, что им нужно EVAL
, но им это не нужно - поскольку выражения вычисляются, и можно также оценить функциональную часть. Большую часть времени использования EVAL
показывает отсутствие понимания оценщика.
Это та же проблема с макросами. Часто начинающие пишут макросы, где они должны писать функции - не понимая, для чего действительно нужны макросы, и не понимая, что функция уже выполняет свою работу.
Часто это неправильный инструмент для работы EVAL
и это часто означает, что новичок не понимает обычных правил оценки Lisp.
Если вы думаете, что вам нужно EVAL
, а затем проверьте, если что-то вроде FUNCALL
, REDUCE
или же APPLY
может быть использован вместо
FUNCALL
- вызвать функцию с аргументами:(funcall '+ 1 2 3)
REDUCE
- вызвать функцию из списка значений и объединить результаты:(reduce '+ '(1 2 3))
APPLY
- вызвать функцию со списком в качестве аргументов:(apply '+ '(1 2 3))
,
Q: мне действительно нужен eval или компилятор / оценщик уже того, чего я действительно хочу?
Основные причины, чтобы избежать 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
путем оценки переменной:
(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
: это часто используется для уродливых хаков.
eval
(на любом языке) не является злом так же, как бензопила не является злом. Это инструмент. Это мощный инструмент, который при неправильном использовании может разорвать конечности и потрошить (образно говоря), но то же самое можно сказать и о многих инструментах в наборе инструментов для программиста, включая:
goto
и друзья- основанный на блокировке поток
- продолжения
- макросы (гигиенические или другие)
- указатели
- перезапускаемые исключения
- самоизменяющийся код
- ... и бросок тысяч.
Если вам нужно использовать какой-либо из этих мощных, потенциально опасных инструментов, спросите себя три раза: "Почему?". в цепочке. Например:
"Почему я должен использовать
eval
? "" Из-за foo. "" Почему foo необходимо? "" Потому что... "
Если вы дойдете до конца этой цепочки, а инструмент все еще выглядит так, как будто это правильно, то сделайте это. Документируйте ад из этого. Испытай ад из этого. Перепроверьте правильность и безопасность снова и снова и снова. Но сделай это.
Эвал в порядке, если вы точно знаете, что происходит в нем. Любой входящий в него пользовательский ДОЛЖЕН быть проверен и проверен и все. Если вы не знаете, как быть на 100% уверенным, то не делайте этого.
По сути, пользователь может ввести любой код для рассматриваемого языка, и он будет выполнен. Вы можете себе представить, какой урон он может нанести.
"Когда я должен использовать eval
?"может быть, лучший вопрос.
Краткий ответ: "когда ваша программа предназначена для написания другой программы во время выполнения, а затем запуска ее". Генетическое программирование является примером ситуации, когда, вероятно, имеет смысл использовать eval
,
ИМО, этот вопрос не относится к ЛИСП. Вот ответ на тот же вопрос для PHP, и он применим к LISP, Ruby и другим другим языкам, имеющим eval:
Основные проблемы с eval ():
- Потенциально небезопасный ввод. Передача ненадежного параметра - способ потерпеть неудачу. Зачастую убедиться, что параметр (или его часть) полностью доверен, не является тривиальной задачей.
- Trickyness. Использование eval () делает код более умным, поэтому его сложнее выполнять. По словам Брайана Кернигана: "Отладка в два раза сложнее, чем писать код в первую очередь. Поэтому, если вы пишете код настолько умно, насколько это возможно, вы, по определению, недостаточно умны, чтобы его отлаживать"
Основная проблема с фактическим использованием eval () только одна:
- неопытные разработчики, которые используют его без достаточного внимания.
Взято отсюда.
Я думаю, что хитрость - это удивительный момент. Одержимость кодом гольф и лаконичный код всегда приводили к "умному" коду (для которого уловки - отличный инструмент). Но вы должны написать свой код для удобства чтения, IMO, чтобы не демонстрировать, что вы умны, и не экономить бумагу (вы все равно не будете печатать его).
Затем в LISP есть некоторая проблема, связанная с контекстом, в котором запускается eval, поэтому ненадежный код может получить доступ к большему количеству вещей; эта проблема кажется распространенной в любом случае.
Было много отличных ответов, но вот еще один пример Мэтью Флэтта, одного из разработчиков Racket:
http://blog.racket-lang.org/2011/10/on-eval-in-dynamic-languages-generally.html
Он делает многие из пунктов, которые уже были рассмотрены, но некоторые люди могут найти его дубль тем не менее интересным.
Резюме: контекст, в котором он используется, влияет на результат eval, но часто не учитывается программистами, что приводит к неожиданным результатам.
Канонический ответ - держаться подальше. Что я нахожу странным, потому что это примитив, и из семи примитивов (остальные - "против", "машина", "cdr", "если" и "цитата"), он получает наименьшее количество пользы и любви.
От On Lisp: "Обычно звонить по eval явно, как покупать что-то в магазине подарков в аэропорту. Дождавшись последнего момента, вы должны заплатить высокие цены за ограниченный выбор второсортных товаров".
Так, когда я использую eval? Одно из обычных применений - иметь REPL в своем REPL, оценивая (loop (print (eval (read))))
, Все в порядке с этим использованием.
Но вы также можете определять функции в терминах макросов, которые будут оцениваться после компиляции, комбинируя eval с обратной кавычкой. Ты иди
(eval `(macro ,arg0 ,arg1 ,arg2))))
и это убьет контекст для вас.
Суонк (для слизи 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, когда вы действительно хотели использовать макрос чтения '#.' который оценивает во время чтения.
Как в "правиле" GOTO: если вы не знаете, что делаете, вы можете создать беспорядок.
Помимо создания чего-то только из известных и безопасных данных, существует проблема, заключающаяся в том, что некоторые языки / реализации не могут достаточно оптимизировать код. Вы можете получить интерпретированный код внутри eval
,
Эвал просто неуверен. Например, у вас есть следующий код:
eval('
hello('.$_GET['user'].');
');
Теперь пользователь заходит на ваш сайт и вводит URL http://example.com/file.php?user=); $ is_admin = true; echo (
Тогда результирующий код будет:
hello();$is_admin=true;echo();
Эвал не злой. Эвал не сложный. Это функция, которая составляет список, который вы ей передаете. В большинстве других языков компиляция произвольного кода будет означать изучение AST языка и копаться во внутренностях компилятора, чтобы выяснить API компилятора. В lisp ты просто называешь eval.
Когда вы должны использовать это? Всякий раз, когда вам нужно что-то скомпилировать, обычно это программа, которая принимает, генерирует или изменяет произвольный код во время выполнения.
Когда вы не должны использовать это? Все остальные случаи.
Почему бы вам не использовать его, когда вам не нужно? Потому что вы будете делать что-то излишне сложным способом, который может вызвать проблемы с читабельностью, производительностью и отладкой.
Да, но если я новичок, как я узнаю, стоит ли мне его использовать? Всегда старайтесь реализовать то, что вам нужно, с помощью функций. Если это не сработает, добавьте макросы. Если это все еще не работает, тогда eval!
Следуйте этим правилам, и вы никогда не будете делать зло с помощью eval:)
Мне очень нравится ответ Зака, и он понял суть вопроса: eval используется, когда вы пишете новый язык, сценарий или модификацию языка. Он на самом деле не объясняет дальше, поэтому я приведу пример:
(eval (read-line))
В этой простой программе на Лиспе пользователю предлагается ввести данные, а затем все, что они ввели, оценивается. Чтобы это работало, должен присутствовать весь набор определений символов, если программа скомпилирована, потому что вы не знаете, какие функции может вводить пользователь, поэтому вам нужно включить их все. Это означает, что если вы скомпилируете эту простую программу, получающийся двоичный файл будет гигантским.
В принципе, вы даже не можете считать это компилируемым утверждением по этой причине. В общем, как только вы используете eval, вы работаете в интерпретируемой среде, и код больше не может быть скомпилирован. Если вы не используете eval, вы можете скомпилировать программу на Лиспе или Scheme, как и программу на Си. Поэтому вы должны убедиться, что хотите и должны находиться в интерпретируемой среде, прежде чем использовать eval.