Perché esattamente Eval è malvagio?


141

So che i programmatori Lisp e Scheme di solito dicono che evaldovrebbero essere evitati se non strettamente necessari. Ho visto la stessa raccomandazione per diversi linguaggi di programmazione, ma non ho ancora visto un elenco di argomenti chiari contro l'uso di eval. Dove posso trovare un resoconto dei potenziali problemi di utilizzoeval ?

Ad esempio, conosco i problemi della GOTOprogrammazione procedurale (rende i programmi illeggibili e difficili da mantenere, rende i problemi di sicurezza difficili da trovare, ecc.), Ma non ho mai visto gli argomenti controeval .

È interessante notare che gli stessi argomenti contro GOTOdovrebbero essere validi contro le continuazioni, ma vedo che gli Schemers, per esempio, non diranno che le continuazioni sono "malvagie" - dovresti solo stare attento quando le usi. Sono molto più propensi a disapprovare il codice usando evalpiuttosto che il codice usando continuazioni (per quanto posso vedere - potrei sbagliarmi).


5
eval non è il male, ma il male è ciò che fa eval
Anurag

9
@yar - Penso che il tuo commento indichi una visione del mondo molto incentrata sull'oggetto. Probabilmente è valido per la maggior parte delle lingue, ma sarebbe diverso in Common Lisp, dove i metodi non appartengono alle classi e ancor più diversi in Clojure, dove le classi sono supportate solo tramite le funzioni di interoperabilità Java. Jay ha etichettato questa domanda come Scheme, che non ha alcuna nozione incorporata di classi o metodi (varie forme di OO sono disponibili come librerie).
Zak,

3
@Zak, hai ragione, conosco solo le lingue che conosco, ma anche se lavori con un documento di Word senza usare gli stili non sei ASCIUTTO. Il mio punto era usare la tecnologia per non ripetere te stesso. OO non è universale, vero ...
Dan Rosenstark

4
Mi sono preso la libertà di aggiungere il tag clojure a questa domanda, poiché credo che gli utenti Clojure possano trarre vantaggio dall'esposizione alle risposte eccellenti pubblicate qui.
Michał Marczyk,

... beh, per Clojure, si applica almeno un motivo in più: si perde la compatibilità con ClojureScript e i suoi derivati.
Charles Duffy,

Risposte:


148

Ci sono diversi motivi per cui non si dovrebbe usare EVAL.

Il motivo principale per i principianti è: non ne hai bisogno.

Esempio (presupponendo Common Lisp):

VALUTARE un'espressione con diversi operatori:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (eval (list op 1 2 3)))))

È meglio scritto come:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (funcall op 1 2 3))))

Ci sono molti esempi in cui i principianti che imparano Lisp pensano di aver bisogno EVAL, ma non ne hanno bisogno, poiché le espressioni vengono valutate e si può anche valutare la parte della funzione. Il più delle volte l'uso di EVALmostra una mancanza di comprensione del valutatore.

È lo stesso problema con le macro. Spesso i principianti scrivono macro, dove dovrebbero scrivere funzioni - non capire a cosa servono veramente le macro e non capire che una funzione fa già il lavoro.

Spesso è lo strumento sbagliato da utilizzare per il lavoro EVALe spesso indica che il principiante non capisce le solite regole di valutazione Lisp.

Se pensi di aver bisogno EVAL, quindi verificare se qualcosa di simile FUNCALL, REDUCEo APPLYpotrebbe essere usato al posto.

  • FUNCALL - chiama una funzione con argomenti: (funcall '+ 1 2 3)
  • REDUCE - chiama una funzione su un elenco di valori e combina i risultati: (reduce '+ '(1 2 3))
  • APPLY- chiamare una funzione con una lista come argomenti: (apply '+ '(1 2 3)).

D: Ho davvero bisogno di eval o il compilatore / valutatore fa già quello che voglio davvero?

I motivi principali da evitare EVALper utenti leggermente più avanzati:

  • vuoi assicurarti che il tuo codice sia compilato, perché il compilatore può controllare il codice per molti problemi e genera codice più veloce, a volte MOLTO MOLTO MOLTO (che è il fattore 1000 ;-)) codice più veloce

  • il codice costruito e che deve essere valutato non può essere compilato il prima possibile.

  • la valutazione di input dell'utente arbitrario apre problemi di sicurezza

  • un certo uso della valutazione EVALpuò avvenire nel momento sbagliato e creare problemi di costruzione

Per spiegare l'ultimo punto con un esempio semplificato:

(defmacro foo (a b)
  (list (if (eql a 3) 'sin 'cos) b))

Quindi, potrei voler scrivere una macro che in base al primo parametro utilizza SINo COS.

(foo 3 4)fa (sin 4)e (foo 1 4)fa (cos 4).

Ora potremmo avere:

(foo (+ 2 1) 4)

Questo non dà il risultato desiderato.

Uno potrebbe quindi voler riparare la macro FOOEVALuando la variabile:

(defmacro foo (a b)
  (list (if (eql (eval a) 3) 'sin 'cos) b))

(foo (+ 2 1) 4)

Ma poi questo non funziona ancora:

(defun bar (a b)
  (foo a b))

Il valore della variabile non è noto al momento della compilazione.

Un motivo generale importante da evitare EVAL: è spesso usato per brutti hack.


3
Grazie! Semplicemente non ho capito l'ultimo punto (valutazione al momento sbagliato?) - potresti elaborare un po 'per favore?
Jay,

41
+1 poiché questa è la vera risposta: le persone ricadono evalsemplicemente perché non sanno che esiste una lingua specifica o una funzione di libreria per fare ciò che vogliono fare. Esempio simile da JS: voglio ottenere una proprietà da un oggetto usando un nome dinamico, quindi scrivo: eval("obj.+" + propName)quando avrei potuto scrivere obj[propName].
Daniel Earwicker,

Capisco cosa intendi adesso, Rainer! Thansk!
Jay,

@ Daniel: "obj.+"? L'ultima volta che ho controllato, +non è valido quando si usano i punti di riferimento in JS.
Ciao,

2
@Daniel probabilmente significava eval ("obj." + PropName) che dovrebbe funzionare come previsto.
Claj,

41

eval(in qualsiasi lingua) non è male allo stesso modo in cui una motosega non è cattiva. È uno strumento. Capita di essere un potente strumento che, se usato in modo improprio, può recidere gli arti ed eviscerare (metaforicamente parlando), ma lo stesso si può dire per molti strumenti nella cassetta degli attrezzi di un programmatore, tra cui:

  • goto e amici
  • threading basato su lock
  • continuazioni
  • macro (igieniche o di altro tipo)
  • puntatori
  • eccezioni riavviabili
  • codice automodificante
  • ... e un cast di migliaia.

Se ti ritrovi a dover utilizzare uno di questi strumenti potenti e potenzialmente pericolosi, chiediti tre volte "perché?" in una catena. Per esempio:

"Perché devo usare eval?" "Per via del foo." "Perché è necessario foo?" "Perché ..."

Se arrivi alla fine di quella catena e lo strumento sembra ancora che sia la cosa giusta da fare, allora fallo. Documenta l'inferno. Metti alla prova l'inferno. Ricontrolla la correttezza e la sicurezza più e più volte. Ma fallo.


Grazie - è quello che ho sentito prima di eval ("chiediti perché"), ma non avevo ancora sentito o letto quali fossero i potenziali problemi. Vedo ora dalle risposte qui quali sono (problemi di sicurezza e prestazioni).
Jay,

8
E leggibilità del codice. Eval può totalmente rovinare il flusso del codice e renderlo incomprensibile.
SOLO IL MIO OPINIONE corretta

Non capisco perché il "threading basato su lock" [sic] sia nella tua lista. Esistono forme di concorrenza che non coinvolgono i blocchi e i problemi con i blocchi sono generalmente ben noti, ma non ho mai sentito nessuno descrivere i blocchi come "cattivi".
asveikau,

4
asveikau: il threading basato su lock è notoriamente difficile da ottenere correttamente (immagino che il 99,44% del codice di produzione che utilizza i lock sia cattivo). Non compone. È incline a trasformare il codice "multi-thread" in codice seriale. (La correzione per questo rende invece il codice lento e gonfio.) Esistono buone alternative al threading basato su lock, come i modelli STM o di attori, che ne fanno cattivo uso in qualsiasi cosa tranne il codice di livello più basso.
SOLO IL MIO OPINIONE corretta,

il "perché catena" :) assicurati di smettere dopo 3 passaggi può far male.
szymanowski

27

Eval va bene, a patto che tu sappia ESATTAMENTE cosa sta succedendo. Qualsiasi input dell'utente che vi accede DEVE essere verificato e validato e tutto il resto. Se non sai come essere sicuro al 100%, allora non farlo.

Fondamentalmente, un utente può digitare qualsiasi codice per la lingua in questione e verrà eseguito. Puoi immaginare da solo quanti danni può fare.


1
Quindi, se in realtà sto generando espressioni S basate sull'input dell'utente usando un algoritmo che non copierà direttamente l'input dell'utente, e se è più facile e più chiaro in una situazione specifica rispetto all'utilizzo di macro o altre tecniche, allora suppongo che non ci sia nulla di "malvagio " a proposito? In altre parole, gli unici problemi con eval sono gli stessi con le query SQL e altre tecniche che utilizzano direttamente l'input dell'utente?
Jay,

10
Il motivo per cui si chiama "male" è perché farlo nel modo sbagliato è molto peggio che fare altre cose nel modo sbagliato. E come sappiamo, i neofiti faranno cose sbagliate.
Tor Valamo,

3
Non direi che il codice deve essere validato prima di valutarlo in ogni circostanza. Quando si implementa un semplice REPL, ad esempio, probabilmente si dovrebbe semplicemente inserire l'input in eval deselezionato e questo non sarebbe un problema (ovviamente quando si scrive un REPL basato sul web avresti bisogno di un sandbox, ma non è il caso del normale CLI-REPL eseguiti sul sistema dell'utente).
sepp2k,

1
Come ho detto, devi sapere esattamente cosa succede quando dai da mangiare a quello che dai alla valutazione. Se ciò significa "eseguirà alcuni comandi entro i limiti della sandbox", questo è ciò che significa. ;)
Tor Valamo

@TorValamo ha mai sentito parlare di una pausa in prigione?
Loïc Faure-Lacroix il

21

"Quando dovrei usare eval?" potrebbe essere una domanda migliore.

La risposta breve è "quando il programma è destinato a scrivere un altro programma in fase di esecuzione e quindi eseguirlo". La programmazione genetica è un esempio di una situazione in cui probabilmente ha senso usare eval.


14

IMO, questa domanda non è specifica per LISP . Ecco una risposta sulla stessa domanda per PHP, e si applica a LISP, Ruby e altre lingue che hanno una valutazione:

I principali problemi con eval () sono:

  • Potenziale input non sicuro.Passare un parametro non attendibile è un modo per fallire. Spesso non è un compito banale assicurarsi che un parametro (o parte di esso) sia completamente attendibile.
  • Trickyness. L'uso di eval () rende il codice intelligente, quindi più difficile da seguire. Per citare Brian Kernighan "Il debug è due volte più difficile della scrittura del codice in primo luogo. Pertanto, se si scrive il codice nel modo più intelligente possibile, non si è, per definizione, abbastanza intelligenti da eseguire il debug "

Il problema principale con l'uso effettivo di eval () è solo uno:

  • sviluppatori inesperti che lo usano senza sufficiente considerazione.

Tratto da qui .

Penso che il trucco sia un punto sorprendente. L'ossessione per il codice golf e il codice conciso ha sempre portato a un codice "intelligente" (per il quale gli eval sono un ottimo strumento). Ma dovresti scrivere il tuo codice per leggibilità, IMO, per non dimostrare che sei un furbo e non per risparmiare carta (non lo stamperai comunque).

Quindi in LISP c'è qualche problema relativo al contesto in cui viene eseguito eval, quindi il codice non attendibile potrebbe accedere a più cose; questo problema sembra essere comune comunque.


3
Il problema dell '"input malvagio" con EVAL riguarda solo le lingue non Lisp, perché in quelle lingue, eval () prende tipicamente un argomento stringa e l'input dell'utente è tipicamente inserito. L'utente può includere una citazione nel proprio input e scappare in il codice generato. Ma in Lisp, l'argomento di EVAL non è una stringa e l'input dell'utente non può sfuggire al codice a meno che tu non sia assolutamente spericolato (come in te hai analizzato l'input con READ-FROM-STRING per creare un'espressione S, che poi includi in il codice EVAL senza virgolette. Se lo citate, non c'è modo di sfuggire alla citazione).
Getta via l'account

12

Ci sono state molte grandi risposte, ma ecco un'altra interpretazione di Matthew Flatt, uno degli implementatori di Racket:

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

Presenta molti dei punti che sono già stati trattati, ma alcune persone potrebbero comunque trovare interessante il suo approccio.

Riepilogo: il contesto in cui viene utilizzato influisce sul risultato di valutazione ma spesso non viene considerato dai programmatori, portando a risultati imprevisti.


11

La risposta canonica è di stare alla larga. Che trovo strano, perché è un primitivo, e dei sette primitivi (gli altri sono contro, auto, cdr, if, eq e quote), ottiene di gran lunga la minima quantità di uso e amore.

Da On Lisp : "Di solito, chiamare eval in modo esplicito è come comprare qualcosa in un negozio di articoli da regalo in aeroporto. Dopo aver atteso fino all'ultimo momento, devi pagare prezzi elevati per una selezione limitata di prodotti di seconda categoria."

Quindi, quando uso eval? Un uso normale è avere un REPL all'interno del proprio REPL valutando (loop (print (eval (read)))). Tutti stanno bene con quell'uso.

Ma puoi anche definire le funzioni in termini di macro che verranno valutate dopo la compilazione combinando eval con il backquote. Tu vai

(eval `(macro ,arg0 ,arg1 ,arg2))))

e ucciderà il contesto per te.

Swank (per emacs melma) è pieno di questi casi. Sembrano così:

(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))))

Non penso sia un trucco sporco. Lo uso sempre per reintegrare le macro in funzioni.


1
Potresti voler controllare il linguaggio del kernel;)
artemonster il

7

Un'altra coppia di punti su Lisp eval:

  • Valuta in un ambiente globale, perdendo il tuo contesto locale.
  • A volte potresti essere tentato di usare eval, quando in realtà intendevi usare la macro di lettura "#". che valuta al momento della lettura.

Capisco che l'uso di env globale sia vero sia per Common Lisp che per Scheme; è vero anche per Clojure?
Jay,

2
In Scheme (almeno per R7RS, forse anche per R6RS) è necessario passare un ambiente per valutare.
CSL

4

Come la "regola" GOTO: se non sai cosa stai facendo, puoi fare un casino.

Oltre a creare solo dati sicuri e conosciuti, c'è il problema che alcune lingue / implementazioni non sono in grado di ottimizzare abbastanza il codice. Potresti finire con un codice interpretato all'interno eval.


Cosa ha a che fare questa regola con GOTO? C'è qualche funzione in un linguaggio di programmazione con cui non puoi fare casino?
Ken

2
@Ken: non esiste una regola GOTO, quindi le virgolette nella mia risposta. C'è solo un dogma per le persone che hanno paura di pensare da sole. Lo stesso per eval. Ricordo di aver accelerato drasticamente alcuni script Perl usando eval. È uno strumento nella tua cassetta degli attrezzi. I neofiti usano spesso eval quando i costrutti di altre lingue sono più facili / migliori. Ma evitarlo completamente solo per essere cool e per favore le persone dogmatiche?
Stesch

4

Eval è solo insicuro. Ad esempio hai il seguente codice:

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

Ora l'utente accede al tuo sito ed entra nell'URL http://example.com/file.php?user= nell'URL ); $ is_admin = true; echo (

Quindi il codice risultante sarebbe:

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

6
stava parlando di Lisp pensato non php
fmsf

4
@fmsf Stava parlando specificamente di Lisp, ma generalmente evalin qualsiasi lingua che lo possiede.
Skilldrick,

4
@fmsf - questa è in realtà una domanda indipendente dalla lingua. Si applica anche ai linguaggi compilati statici in quanto possono simulare eval chiamando il compilatore in fase di esecuzione.
Daniel Earwicker,

1
in tal caso la lingua è un duplicato. Ho visto un sacco come questo qui intorno.
fmsf,

9
PHP eval non è come Lisp eval. Guarda, funziona su una stringa di caratteri e l'exploit nell'URL dipende dalla possibilità di chiudere una parentesi testuale e aprirne un'altra. Lisp eval non è sensibile a questo tipo di cose. Puoi valutare i dati che arrivano come input da una rete, se li sandbox correttamente (e la struttura è abbastanza facile da camminare per farlo).
Kaz,

2

Eval non è cattivo. Eval non è complicato. È una funzione che compila l'elenco che ci passi. Nella maggior parte delle altre lingue, compilare un codice arbitrario significherebbe apprendere l'AST della lingua e scavare negli interni del compilatore per capire l'API del compilatore. In lisp, chiami solo eval.

Quando dovresti usarlo? Ogni volta che è necessario compilare qualcosa, in genere un programma che accetta, genera o modifica codice arbitrario in fase di esecuzione .

Quando non dovresti usarlo? Tutti gli altri casi

Perché non dovresti usarlo quando non è necessario? Perché si farebbe qualcosa in un modo inutilmente complicato che potrebbe causare problemi di leggibilità, prestazioni e debug.

Sì, ma se sono un principiante come faccio a sapere se dovrei usarlo? Cerca sempre di implementare ciò di cui hai bisogno con le funzioni. Se non funziona, aggiungi le macro. Se ancora non funziona, allora eval!

Segui queste regole e non farai mai del male con eval :)


0

Mi piace molto la risposta di Zak e ha capito l'essenza della questione: eval viene utilizzato quando si scrive una nuova lingua, una sceneggiatura o una modifica di una lingua. In realtà non spiega ulteriormente, quindi farò un esempio:

(eval (read-line))

In questo semplice programma Lisp, all'utente viene richiesto di immettere e quindi viene valutato qualunque cosa inserisca. Perché questo funzioni tutto questo set di definizioni dei simboli deve essere presente se il programma è compilato, perché non hai idea di quali funzioni l'utente possa inserire, quindi devi includerle tutte. Ciò significa che se compili questo semplice programma, il binario risultante sarà gigantesco.

In linea di principio, non puoi nemmeno considerarlo un'affermazione compilabile per questo motivo. In generale, una volta usato eval , si opera in un ambiente interpretato e il codice non può più essere compilato. Se non usi eval , puoi compilare un programma Lisp o Scheme proprio come un programma C. Pertanto, si desidera assicurarsi che si desidera e sia necessario trovarsi in un ambiente interpretato prima di impegnarsi a utilizzare eval .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.