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

141

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

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

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

Джей
источник
5
eval - это не зло, но зло - это то, что делает eval
Anurag
9
@yar - Я думаю, что ваш комментарий указывает на очень однозначный объектно-ориентированный взгляд на мир. Вероятно, это справедливо для большинства языков, но будет по-другому в Common Lisp, где методы не принадлежат классам, и еще больше различаются в Clojure, где классы поддерживаются только через функции взаимодействия Java. Джей пометил этот вопрос как Scheme, в котором нет встроенного понятия классов или методов (различные формы объектно-ориентированного программирования доступны в виде библиотек).
Zak
3
@Zak, ты прав, я знаю только те языки, которые знаю, но даже если вы работаете с документом Word без использования стилей, вы не СУХИЕ. Я хотел использовать технологию, чтобы не повторяться. OO не универсален, правда ...
Дэн Розенстарк
4
Я взял на себя смелость добавить к этому вопросу тег clojure, поскольку считаю, что пользователи Clojure могут извлечь выгоду из замечательных ответов, размещенных здесь.
Michał Marczyk
1
goto"зло", потому что это форма мутации: фактически новое значение внезапно присваивается указателю инструкции. Продолжение не предполагает мутаций; чисто функциональный язык может иметь продолжения. Они более чистые, чем управляющие конструкции вроде if и while , которые Дейкстра устраивал, несмотря на то , что они были легким синтаксическим сахаром по сравнению с goto и метками.
Каз,

Ответы:

150

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

Существует множество примеров, когда новички, изучающие Лисп, думают, что им это нужно 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для немного более продвинутых пользователей:

  • вы хотите убедиться, что ваш код скомпилирован, потому что компилятор может проверять код на наличие многих проблем и генерирует более быстрый код, иногда НАМНОГО НАМНОГО (это фактор 1000 ;-)) более быстрого кода

  • код, который создается и требует оценки, не может быть скомпилирован как можно раньше.

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

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

Райнер Йосвиг
источник
3
Благодарность! Я просто не понял последнего пункта (оценка в неподходящее время?) - не могли бы вы немного уточнить, пожалуйста?
Джей
42
+1, поскольку это реальный ответ - люди отступают evalпросто потому, что не знают, что есть конкретный язык или функция библиотеки, чтобы делать то, что они хотят делать. Аналогичный пример из JS: я хочу получить свойство от объекта, используя динамическое имя, поэтому я пишу: eval("obj.+" + propName)когда я мог бы написать obj[propName].
Дэниел Эрвикер,
Теперь я понимаю, что ты имеешь в виду, Райнер! Спасибо!
Джей
@Daniel: "obj.+"? Последнее, что я проверял, +недействительно при использовании точечных ссылок в JS.
Hello71
2
@Daniel, вероятно, имел в виду eval ("obj." + PropName), который должен работать должным образом.
claj 02
41

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

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

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

"Почему я должен использовать eval?" "Из-за foo." "Зачем нужен foo?" "Так как ..."

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

ТОЛЬКО МОЕ ПРАВИЛЬНОЕ МНЕНИЕ
источник
Спасибо - это то, что я слышал об eval раньше («спросите себя, почему»), но я еще никогда не слышал и не читал, в чем заключаются потенциальные проблемы. Теперь из ответов здесь я понимаю, что это такое (проблемы с безопасностью и производительностью).
Джей
8
И читабельность кода. Eval может полностью испортить поток кода и сделать его непонятным.
ТОЛЬКО МОЕ ПРАВИЛЬНОЕ МНЕНИЕ
Я не понимаю, почему в вашем списке фигурирует "потоковая передача на основе блокировки" [sic]. Существуют формы параллелизма, которые не связаны с блокировками, и проблемы с блокировками в целом хорошо известны, но я никогда не слышал, чтобы кто-нибудь называл использование блокировок «злом».
asveikau
4
asveikau: Распределение потоков на основе блокировок, как известно, сложно сделать правильно (я предполагаю, что 99,44% производственного кода, использующего блокировки, - это плохо). Это не сочиняет. Он склонен к превращению вашего «многопоточного» кода в последовательный. (Исправление этого просто делает код медленным и раздутым.) Существуют хорошие альтернативы потоковой передаче на основе блокировок, такие как STM или модели акторов, которые делают ее использование в чем угодно, кроме самого низкоуровневого кода.
ТОЛЬКО МОЕ ПРАВИЛЬНОЕ МНЕНИЕ
"почему цепочка" :) обязательно останавливаться через 3 шага может больно.
szymanowski
27

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

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

Тор Валамо
источник
1
Так что, если я на самом деле генерирую S-выражения на основе пользовательского ввода с использованием алгоритма, который не будет напрямую копировать пользовательский ввод, и если это проще и понятнее в какой-то конкретной ситуации, чем использование макросов или других методов, то я полагаю, что нет ничего "злого" " об этом? Другими словами, единственные проблемы с eval такие же, как с SQL-запросами и другими методами, которые напрямую используют пользовательский ввод?
Джей
10
Причина, по которой это называется «злом», состоит в том, что делать что-то неправильно намного хуже, чем делать что-то неправильно. А как известно, новички будут делать что-то не так.
Тор Валамо
3
Я бы не сказал, что код должен быть проверен перед тем, как отказываться от него при любых обстоятельствах. Например, при реализации простого REPL вы, вероятно, просто передадите входные данные в eval без отметки, и это не будет проблемой (конечно, при написании веб-REPL вам понадобится песочница, но это не относится к обычным CLI-REPL, которые работают в системе пользователя).
sepp2k 03
1
Как я уже сказал, вы должны точно знать, что происходит, когда вы вводите то, что вы кормите в eval. Если это означает, что «он будет выполнять некоторые команды в пределах песочницы», то это то, что это значит. ;)
Тор Валамо
@TorValamo когда-нибудь слышал о побеге из тюрьмы?
Лоик Фор-Лакруа
22

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

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

Зак
источник
Прекрасный ответ.
Марк
14

ИМО, этот вопрос не относится к LISP . Вот ответ на тот же вопрос для PHP, и он применим к LISP, Ruby и другим другим языкам, в которых есть eval:

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

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

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

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

Взято отсюда .

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

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

Дэн Розенстарк
источник
3
Проблема "злонамеренного ввода" с EVAL затрагивает только языки, отличные от Lisp, потому что в этих языках eval () обычно принимает строковый аргумент, а ввод пользователя обычно вставляется. Пользователь может включить кавычку в свой ввод и выйти в сгенерированный код. Но в Лиспе аргумент EVAL не является строкой, и пользовательский ввод не может уйти в код, если вы не абсолютно безрассудны (например, вы проанализировали ввод с помощью READ-FROM-STRING, чтобы создать S-выражение, которое затем включаете в код EVAL без кавычек. Если вы его процитируете, вы не сможете избежать кавычек).
Выкинуть аккаунт
12

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

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

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

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

Stchang
источник
11

Канонический ответ - держаться подальше. Что я нахожу странным, потому что это примитив, и из семи примитивов (остальные - cons, car, cdr, if, eq и quote) он получает гораздо меньше пользы и любви.

From On Lisp : «Обычно явный вызов eval - это как покупка чего-то в сувенирном магазине в аэропорту. Выждав до последнего момента, вы должны заплатить высокую цену за ограниченный выбор второсортных товаров».

Итак, когда мне использовать eval? Обычное использование - иметь REPL в вашем REPL путем оценки (loop (print (eval (read)))). Всем нравится такое использование.

Но вы также можете определять функции в терминах макросов, которые будут оцениваться после компиляции, комбинируя 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))))

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

Даниэль Куссен
источник
1
Вы можете проверить язык ядра;)
artemonster
7

Еще пара замечаний по оценке Lisp:

  • Он оценивает в глобальной среде, теряя ваш локальный контекст.
  • Иногда у вас может возникнуть соблазн использовать eval, когда вы действительно хотели использовать макрос чтения '#'. который оценивается во время чтения.
pyb
источник
Я понимаю, что использование глобального env верно как для Common Lisp, так и для Scheme; верно ли это и для Clojure?
Jay
2
В Scheme (по крайней мере, для R7RS, возможно, также для R6RS) вы должны передать среду eval.
csl
4

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

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

Stesch
источник
Какое отношение имеет это правило к GOTO? Есть ли в каком-либо языке программирования функция, с которой нельзя напортачить?
Кен
2
@Ken: Нет правила GOTO, поэтому в моем ответе кавычки. Есть просто догма для людей, которые боятся думать самостоятельно. То же самое и с eval. Я помню, как резко ускорил работу некоторых Perl-скриптов с помощью eval. Это один из инструментов в вашем арсенале. Новички часто используют eval, когда другие языковые конструкции проще / лучше. Но полностью избегать этого, чтобы быть крутым и угодить догматичным людям?
Stesch
4

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

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

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

Тогда результирующий код будет:

hello();$is_admin=true;echo();
Рагнис
источник
6
он говорил о Lisp, а не о php
fmsf 03
4
@fmsf Он говорил конкретно о Лиспе, но в целом о evalлюбом языке, в котором он есть.
Skilldrick
4
@fmsf - это вопрос, не зависящий от языка. Это применимо даже к статическим скомпилированным языкам, поскольку они могут имитировать eval, вызывая компилятор во время выполнения.
Дэниел Эрвикер,
1
в этом случае язык является дубликатом. Я видел много подобных здесь.
fmsf 03
9
PHP eval не похож на Lisp eval. Посмотрите, он работает с символьной строкой, и эксплойт в URL-адресе зависит от возможности закрыть текстовую скобку и открыть другую. Lisp eval не восприимчив к подобным вещам. Вы можете оценивать данные, которые поступают в качестве входных данных из сети, если вы правильно поместите их в песочницу (и структура достаточно проста для обхода для этого).
Kaz
2

Эвал не зло. Оценка не сложная. Это функция, которая составляет список, который вы ей передаете. В большинстве других языков компиляция произвольного кода означала бы изучение AST языка и копание во внутренностях компилятора, чтобы выяснить API компилятора. В lisp вы просто вызываете eval.

Когда его использовать? Всякий раз, когда вам нужно что-то скомпилировать, обычно это программа, которая принимает, генерирует или изменяет произвольный код во время выполнения .

Когда нельзя его использовать? Все остальные случаи.

Почему бы вам не использовать его, когда в этом нет необходимости? Потому что вы будете делать что-то излишне сложным способом, что может вызвать проблемы с читабельностью, производительностью и отладкой.

Да, но если я новичок, как мне узнать, стоит ли мне его использовать? Всегда старайтесь реализовать то, что вам нужно, с помощью функций. Если это не сработает, добавьте макросы. Если это все еще не работает, то eval!

Следуйте этим правилам, и вы никогда не сделаете зла с eval :)

Optevo
источник
0

Мне очень нравится ответ Зака, и он понял суть дела: eval используется, когда вы пишете новый язык, сценарий или модифицируете язык. Он не объясняет подробностей, поэтому я приведу пример:

(eval (read-line))

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

По этой причине вы, в принципе, не можете даже считать это заявление компилируемым. В общем, как только вы используете eval , вы работаете в интерпретируемой среде, и код больше не может быть скомпилирован. Если вы не используете eval, вы можете скомпилировать программу на Lisp или Scheme так же, как программу на C. Следовательно, вы хотите убедиться, что хотите и должны находиться в интерпретируемой среде, прежде чем переходить к использованию eval .

Тайлер Дерден
источник