Чому саме таке зло?


141

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

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

Цікаво, що ті ж аргументи проти GOTOповинні бути справедливими проти продовження, але я бачу, що, наприклад, Шемерс не скаже, що продовження є «злим» - вам слід просто бути обережними при їх використанні. Вони набагато частіше нахмуряються на використання коду, evalніж на код, що використовує продовження (наскільки я бачу - я можу помилитися).


5
eval не є злом, але зло - те, що робить eval
Anurag

9
@yar - Я думаю, що ваш коментар вказує на дуже єдиний погляд на світогляд, орієнтований на один предмет. Це, мабуть, справедливо для більшості мов, але відрізняється від Common Lisp, де методи не належать до класів, а ще більше відрізняються у Clojure, де класи підтримуються лише через функції інтеропа Java. Джей позначив це питання схемою, в якій немає ніякого вбудованого поняття про класи або методи (різні форми ОО доступні як бібліотеки).
Зак

3
@Zak, ти маєш рацію, я знаю лише мови, які я знаю, але навіть якщо ти працюєш із документом Word без використання стилів, ти не БУДЕШ. Моя суть полягала в тому, щоб використовувати технологію, щоб не повторюватися. OO не є універсальним, правда ...
Dan Rosenstark

4
Я взяв на себе сміливість додати тег clojure до цього питання, оскільки вважаю, що користувачі Clojure можуть отримати користь від впливу відмінних відповідей, розміщених тут.
Michał Marczyk

... що ж, для Clojure застосовується хоча б одна додаткова причина: Ви втрачаєте сумісність з ClojureScript та його похідними.
Чарльз Даффі

Відповіді:


148

Є кілька причин, чому не слід користуватися EVAL.

Основна причина для початківців: вам це не потрібно.

Приклад (якщо припустити звичайний 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шляхом 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: її часто використовують для некрасивих хак.


3
Дякую! Я просто не зрозумів останнього пункту (оцінювання в неправильний час?) - Куди ви докладно уточнили?
Джей

41
+1, оскільки це справжня відповідь - люди впадають у відповідь evalпросто тому, що не знають, що існує певна мова чи бібліотека, щоб робити те, що вони хочуть робити. Подібний приклад JS: Я хочу отримати властивість від об'єкта, використовуючи динамічне ім'я, тому пишу: eval("obj.+" + propName)коли я міг би написати obj[propName].
Даніель Ервікер

Я бачу, що ти маєш на увазі зараз, Райнер! Танськ!
Джей

@Daniel "obj.+":? Востаннє я перевірив, +недійсний при використанні точкових посилань у JS.
Привіт71

2
@Daniel, ймовірно, означав eval ("obj." + PropName), який повинен працювати як очікувалося.
claj

41

eval(будь-якою мовою) - не зло так само, як бензопила не є злою. Це інструмент. Це може бути потужним інструментом, який при неправильному використанні може порвати кінцівки і викреслити (метафорично кажучи), але те саме можна сказати і для багатьох інструментів в наборі інструментів програміста, включаючи:

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

Якщо вам доводиться користуватися будь-яким із цих потужних, потенційно небезпечних інструментів, запитайте себе тричі "чому?" в ланцюжку. Наприклад:

"Навіщо мені користуватися eval ?" "Через foo." "Чому foo необхідний?" "Тому що ..."

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


Дякую - це те, що я чув про eval раніше ("запитайте себе чому"), але я ще ніколи не чув і не читав, які потенційні проблеми. Зараз з відповідей я бачу, що вони є (проблеми із безпекою та ефективністю роботи).
Джей

8
І читабельність коду. Eval може повністю викрутити потік коду і зробити його незрозумілим.
ПРОСТО МОЕ правильне ДУМКА

Я не розумію, чому "замовлення на основі блокування" [sic] є у вашому списку. Існують форми одночасності, які не передбачають блокування, а проблеми із блокуваннями загальновідомі, але я ніколи не чув, щоб хтось використовував блокування як "злі".
asveikau

4
asveikau: Нарізання різьби на основі блокування, як відомо, важко отримати правильно (я б припустив, що 99,44% виробничого коду з використанням замків погано). Це не складається. Він схильний до перетворення вашого "багатопотокового" коду в серійний код. (Виправлення цього просто робить код повільним і роздутим.) Існують хороші альтернативи блокування на основі блокування, як STM або акторські моделі, що дозволяє використовувати його ні в чому, окрім коду найнижчого рівня, злому.
ДАЙТЕ МОЕ правильне ДУМКА

"чому ланцюжок" :) не забудьте зупинитися після 3 кроків, це може зашкодити.
szymanowski

27

Евал - це добре, якщо ви точно знаєте що в ньому відбувається. Будь-який вхід користувача, що входить до нього, ОБОВ'ЯЗКОВО перевіряється та підтверджується, і все. Якщо ви не знаєте, як бути на 100% впевненим, тоді не робіть цього.

В основному, користувач може ввести будь-який код для відповідної мови, і він буде виконуватися. Ви можете собі уявити, скільки шкоди він може зробити.


1
Тож якщо я фактично генерую S-вирази на основі введення користувача, використовуючи алгоритм, який не буде безпосередньо копіювати введення користувача, і якщо це простіше і зрозуміліше в якійсь конкретній ситуації, ніж використання макросів або інших методів, то я припускаю, що немає нічого "зла " про це? Іншими словами, єдині проблеми з eval - це ті ж самі запити SQL та інші методи, які безпосередньо використовують введення користувача?
Джей

10
Причина, яку називають "злом", полягає в тому, що робити це неправильно - це набагато гірше, ніж робити інші речі неправильно. І як ми знаємо, новачки будуть робити неправильно речі.
Tor Valamo

3
Я б не сказав, що код повинен бути перевірений, перш ніж його оцінювати за будь-яких обставин. Наприклад, застосовуючи просту REPL, ви, ймовірно, просто подаватимете вхід в eval невірно, і це не буде проблемою (звичайно, коли ви пишете веб-REPL, вам знадобиться пісочниця, але це не так для звичайних CLI-REPL, які працюють у системі користувача).
sepp2k

1
Як я вже казав, ви повинні точно знати, що відбувається, коли ви годуєте тим, що годуєте в евалі. Якщо це означає, що "він виконає деякі команди в межах пісочниці", то це означає. ;)
Тор Валамо

@TorValamo коли-небудь чув про перерву в'язниці?
Loïc Faure-Lacroix

21

"Коли я повинен використовувати eval?" може бути краще питання.

Коротка відповідь - "коли ваша програма призначена написати іншу програму під час виконання, а потім запустити її". Генетичне програмування - приклад ситуації, коли це, ймовірно, має сенс використовувати eval.


14

ІМО, це питання не є специфічним для LISP . Ось відповідь на те саме питання для PHP, і він стосується LISP, Ruby та інших інших мов, які мають огляд:

Основні проблеми з eval ():

  • Потенційний небезпечний вхід.Передача ненадійного параметра - це спосіб провалу. Часто не є тривіальним завданням переконатись у повному довірі до параметра (або його частини).
  • Хитрість. Використання eval () робить код розумним, тому важче дотримуватися. Цитувати Брайана Керніган " Налагодження вдвічі складніше, ніж писати код в першу чергу. Тому, якщо ви пишете код якомога розумніше, ви, за визначенням, недостатньо розумні, щоб налагодити його "

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

  • недосвідчені розробники, які використовують його без достатньої уваги.

Взято звідси .

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

Тоді в LISP є певна проблема, пов'язана з контекстом, в якому запускається eval, тому ненадійний код може отримати доступ до більшої кількості речей; ця проблема, здається, є загальною.


3
Проблема "злого введення" з EVAL впливає лише на мови, які не є Lisp, оскільки в цих мовах eval () зазвичай бере аргумент рядка, а введення користувача, як правило, сплановане. згенерований код. Але в Lisp аргумент EVAL - це не рядок, і введення користувача не може вийти з коду, якщо ви абсолютно нерозсудливі (як ви розібрали вхід з READ-FROM-STRING, щоб створити S-вираз, який ви потім включаєте в код EVAL, не цитуючи його. Якщо ви цитуєте його, немає можливості уникнути цитати).
Викиньте рахунок

12

Було багато чудових відповідей, але ось ще один прийом від Меттью Флатта, одного з реалізаторів Racket:

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

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

Короткий зміст: Контекст, в якому він використовується, впливає на результат eval, але програмісти часто не розглядаються, що призводить до несподіваних результатів.


11

Канонічна відповідь - триматися подалі. Що мені здається дивним, оскільки це примітив, і з семи примітивів (інші - мінуси, машина, компакт-диск, якщо, еквівалент і цитата) - це отримує далеко і найменший обсяг використання та любові.

З Ліпп : "Зазвичай, дзвонити в 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))))

Я не думаю, що це брудний злом. Я весь цей час використовую сам для реінтеграції макросів у функції.


1
Ви можете перевірити мову ядра;)
artemonster

7

Ще одна кількість балів на Lisp eval:

  • Він оцінює в глобальному середовищі, втрачаючи локальний контекст.
  • Іноді ви можете спокуситись використовувати eval, коли ви дійсно мали намір використовувати макрос читання "#". який оцінює під час читання.

Я розумію, що використання глобальної програми env справедливо як для Common Lisp, так і для Scheme; це правда і для Clojure?
Jay

2
У Схемі (принаймні для R7RS, можливо, також і для R6RS) ви повинні пройти середовище для оцінювання.
csl

4

Як і правило "GOTO": Якщо ви не знаєте, що ви робите, ви можете заплутатися.

Окрім того, що лише будувати щось із відомих та безпечних даних, існує проблема, що деякі мови / реалізації не можуть достатньо оптимізувати код. Можна закінчити інтерпретований код всередині eval.


Яке це правило має відношення до GOTO? Чи є якась функція в будь-якій мові програмування, з якою ви не можете поплутати?
Кен

2
@Ken: Правила GOTO немає, отже, лапки у моїй відповіді. Існує просто догма для людей, які бояться думати про себе. Те саме для eval. Я пам’ятаю, що різко прискорив деякий сценарій Perl, використовуючи eval. Це один інструмент у вашій панелі інструментів. Новачки часто використовують eval, коли інші мовні конструкції легші / кращі. Але уникати цього повністю просто для того, щоб бути крутими і радувати догматичних людей?
stesch

4

Евал просто незахищений. Наприклад, у вас є такий код:

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

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

Тоді отриманий код буде таким:

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

6
він говорив про Lisp думав не php
fmsf

4
@fmsf Він говорив спеціально про Лісп, але загалом про evalбудь-яку мову, яка його має.
Skilldrick

4
@fmsf - це власне питання, яке не залежить від мови. Це стосується навіть статичних компільованих мов, оскільки вони можуть імітувати eval, викликаючи компілятор під час виконання.
Даніель Ервікер

1
у цьому випадку мова є дублікатом. Тут я бачив багато подібних.
fmsf

9
PHP eval не схожий на Lisp eval. Подивіться, він працює на символьному рядку, і використання URL-адреси залежить від того, чи зможете закрити текстові дужки та відкрити ще одну. Lisp eval не сприйнятливий до подібних матеріалів. Ви можете оцінювати дані, які надходять як мережа вхідних даних, якщо ви належним чином розміщуєте їх в пісочниці (і структура достатньо легко ходити для того, щоб це зробити).
Каз

2

Евал - не зло. Евал не складний. Це функція, яка компілює список, який ви йому передаєте. У більшості інших мов компіляція довільного коду означатиме вивчення AST мови та копання у внутрішніх програмах компілятора, щоб визначити API компілятора. У lisp, ви просто називаєте eval.

Коли його слід використовувати? Кожен раз, коли вам потрібно щось скласти, зазвичай це програма, яка приймає, генерує або модифікує довільний код під час виконання .

Коли ви не повинні його використовувати? Усі інші випадки.

Чому б не використовувати його, коли не потрібно? Тому що ви б робили щось непотрібно складним способом, що може спричинити проблеми для читабельності, продуктивності та налагодження.

Так, але якщо я початківець, то як мені знати, чи варто його використовувати? Завжди намагайтеся реалізувати те, що вам потрібно, за допомогою функцій. Якщо це не працює, додайте макроси. Якщо це все ще не працює, тоді eval!

Дотримуйтесь цих правил, і ви ніколи не будете робити зло з eval :)


0

Мені дуже подобається відповідь Зака, і він зрозумів суть справи: eval використовується, коли ви пишете нову мову, сценарій чи модифікацію мови. Він насправді не пояснює далі, тому я наведу приклад:

(eval (read-line))

У цій простій програмі Lisp користувачеві пропонується ввести дані, а потім оцінюється все, що вони вводять. Щоб це працювало, цілий набір визначень символів повинен бути присутнім, якщо програма складена, оскільки ви не маєте поняття, які функції може вводити користувач, тому ви повинні включити їх усі. Це означає, що якщо скласти цю просту програму, отриманий двійковий файл буде гігантським.

В принципі, ви навіть не можете вважати це складеним твердженням з цієї причини. Загалом, як тільки ви користуєтеся eval ви працюєте в інтерпретованому середовищі, і код більше не можна компілювати. Якщо ви не використовуєте eval, ви можете скласти програму Lisp або Scheme так само, як і програму C. Тому ви хочете переконатися, що хочете і вам потрібно опинитися в інтерпретованому середовищі, перш ніж взяти участь у використанні eval .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.