Почему именно зло? - PullRequest
       74

Почему именно зло?

140 голосов
/ 03 апреля 2010

Я знаю, что программисты на Лиспе и Схеме обычно говорят, что eval следует избегать, если в этом нет особой необходимости. Я видел одну и ту же рекомендацию для нескольких языков программирования, но я еще не видел список четких аргументов против использования eval. Где я могу найти информацию о потенциальных проблемах использования eval?

Например, я знаю проблемы GOTO в процедурном программировании (делает программы нечитаемыми и сложными в обслуживании, затрудняет поиск проблем безопасности и т. Д.), Но я никогда не видел аргументов против eval.

Интересно, что те же аргументы против GOTO должны быть действительны против продолжений, но я вижу, что, например, Схемерс не скажет, что продолжения являются "злом" - вы должны быть осторожны при их использовании. Скорее всего, они будут недовольны кодом, использующим eval, чем кодом, использующим продолжения (насколько я понимаю - я могу ошибаться).

Ответы [ 12 ]

146 голосов
/ 03 апреля 2010

Есть несколько причин, по которым нельзя использовать 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, то проверьте, можно ли использовать что-то вроде 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, EVALuating переменную:

(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: это часто используется для уродливых хаков.

41 голосов
/ 03 апреля 2010

eval (на любом языке) не является злом так же, как бензопила не является злом. Это инструмент. Это мощный инструмент, который при неправильном использовании может разорвать конечности и потрошить (образно говоря), но то же самое можно сказать и о многих инструментах в наборе инструментов программиста, включая:

  • goto и друзья
  • потоковая обработка на основе блокировки
  • продолжений
  • макросы (гигиенические или другие)
  • указатели
  • перезапускаемые исключения
  • самоизменяющийся код
  • ... и бросок тысяч.

Если вам нужно использовать какой-либо из этих мощных, потенциально опасных инструментов, спросите себя три раза: «Почему?» в цепочке. Например:

"Почему я должен использовать eval?" "Из-за фу". "Почему Фу необходимо? "" Потому что ... "

Если вы дойдете до конца этой цепочки, а инструмент все еще выглядит так, как будто это правильно, то сделайте это. Документируйте ад из этого. Испытай ад из этого. Перепроверьте правильность и безопасность снова и снова и снова. Но сделай это.

26 голосов
/ 03 апреля 2010

Эвал в порядке, если вы знаете ТОЧНО что входит в него.Любой пользовательский ввод, входящий в него, ДОЛЖЕН быть проверен и проверен, и все.Если вы не знаете, как быть на 100% уверенным, не делайте этого.

По сути, пользователь может ввести любой код для рассматриваемого языка, и он выполнится.Вы можете себе представить, какой урон он может нанести.

21 голосов
/ 04 апреля 2010

"Когда я должен использовать eval?" может быть, лучший вопрос.

Краткий ответ: «когда ваша программа предназначена для написания другой программы во время выполнения, а затем ее запуска». Генетическое программирование является примером ситуации, когда, вероятно, имеет смысл использовать eval.

14 голосов
/ 03 апреля 2010

IMO, этот вопрос не относится к LISP . Вот ответ на тот же вопрос для PHP, и он применим к LISP, Ruby и другим другим языкам, имеющим eval:

Основные проблемы с eval ():

  • Потенциально небезопасный ввод. Передача ненадежного параметра - это способ потерпеть поражение. Часто это не тривиальная задача чтобы убедиться, что параметр (или часть из этого) полностью доверяют.
  • Хитрость. Использование eval () делает код более умным, а значит более сложным следить. Цитировать Брайана Кернигана " Отладка в два раза сложнее, чем писать код в первую очередь. Поэтому, если вы напишите код как умно, насколько это возможно, вы, по определение, недостаточно умное для отладки он"

Основная проблема с фактическим использованием eval () только один:

  • неопытные разработчики, которые используют его без достаточного внимания.

Взято из здесь .

Я думаю, что хитрость - это удивительный момент. Одержимость кодом гольф и лаконичный код всегда приводили к «умному» коду (для которого уловки - отличный инструмент). Но вы должны написать свой код для удобства чтения, IMO, чтобы не демонстрировать, что вы умны и не экономить бумагу (вы все равно не будете печатать его).

Тогда в LISP есть некоторая проблема, связанная с контекстом, в котором запускается eval, поэтому ненадежный код может получить доступ к большему количеству вещей; в любом случае эта проблема, кажется, распространена.

12 голосов
/ 14 марта 2013

Было много отличных ответов, но вот еще один пример Мэтью Флэтта, одного из разработчиков Racket:

http://blog.racket -lang.org / 2011/10 / на Eval-в-динамическом РоботыОперационные-generally.html

Он делает многие из пунктов, которые уже были рассмотрены, но некоторые люди могут найти его дубль тем не менее интересным.

Резюме: контекст, в котором он используется, влияет на результат eval, но часто не учитывается программистами, что приводит к неожиданным результатам.

11 голосов
/ 29 октября 2010

Канонический ответ - держаться подальше. Что я нахожу странным, потому что это примитив, и из семи примитивов (остальные - «против», «машина», «cdr», «если» и «цитата»), он получает наименьшее количество пользы и любви.

С На Лиспе : «Обычно явный вызов 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))))

Я не думаю, что это грязный хак. Я все время использую его для реинтеграции макросов в функции.

7 голосов
/ 31 октября 2010

Еще пара моментов на Lisp eval:

  • Он оценивает в глобальной среде, теряя ваш локальный контекст.
  • Иногда у вас может возникнуть желание использовать eval, когда вы действительно хотели использовать макрос чтения '#.' который оценивает во время чтения.
4 голосов
/ 03 апреля 2010

Eval просто небезопасен. Например, у вас есть следующий код:

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

Теперь пользователь заходит на ваш сайт и вводит URL http://example.com/file.php?user=);$is_admin=true;echo(

Тогда полученный код будет:

hello();$is_admin=true;echo();
4 голосов
/ 03 апреля 2010

Как и в «правиле» GOTO: если вы не знаете, что делаете, вы можете создать беспорядок.

Помимо того, что вы создаете что-то только из известных и безопасных данных, существует проблема, заключающаяся в том, что некоторыеязыки / реализации не могут оптимизировать код достаточно.Вы можете получить интерпретированный код внутри eval.

...