Tại sao chính xác là xấu xa eval?


141

Tôi biết rằng các lập trình viên Lisp và Scheme thường nói rằng evalnên tránh nếu không cần thiết. Tôi đã thấy đề xuất tương tự cho một số ngôn ngữ lập trình, nhưng tôi chưa thấy một danh sách các đối số rõ ràng chống lại việc sử dụng eval. Tôi có thể tìm tài khoản về các vấn đề tiềm ẩn khi sử dụng ở evalđâu?

Ví dụ, tôi biết các vấn đề GOTOtrong lập trình thủ tục (làm cho các chương trình không thể đọc được và khó bảo trì, làm cho các vấn đề bảo mật khó tìm, v.v.), nhưng tôi chưa bao giờ thấy các đối số chống lại eval.

Thật thú vị, các lý lẽ tương tự chống lại GOTOnên có giá trị đối với các phần tiếp theo, nhưng tôi thấy rằng Schemers, chẳng hạn, sẽ không nói rằng phần tiếp theo là "xấu xa" - bạn chỉ nên cẩn thận khi sử dụng chúng. Họ có nhiều khả năng nhăn mặt khi sử dụng mã evalhơn là sử dụng mã liên tục (theo như tôi có thể thấy - tôi có thể sai).


5
eval không phải là xấu xa, nhưng ác là những gì eval làm
Anurag

9
@yar - Tôi nghĩ rằng nhận xét của bạn cho thấy một thế giới quan rất đơn lẻ-đối tượng-trung tâm. Nó có thể hợp lệ đối với hầu hết các ngôn ngữ, nhưng sẽ khác với Common Lisp, nơi các phương thức không thuộc về các lớp và thậm chí khác hơn trong Clojure, nơi các lớp chỉ được hỗ trợ thông qua các hàm interop Java. Jay đã gắn thẻ câu hỏi này là Scheme, không có bất kỳ khái niệm tích hợp nào về các lớp hoặc phương thức (các dạng OO khác nhau có sẵn dưới dạng thư viện).
Zak

3
@Zak, bạn đã đúng, tôi chỉ biết các ngôn ngữ tôi biết, nhưng ngay cả khi bạn đang làm việc với một tài liệu Word mà không sử dụng Kiểu bạn không bị KHÔ. Quan điểm của tôi là sử dụng công nghệ để không lặp lại chính mình. OO không phải là phổ quát, đúng ...
Dan Rosenstark

4
Tôi đã tự do thêm thẻ clojure vào câu hỏi này, vì tôi tin rằng người dùng Clojure có thể được hưởng lợi từ việc tiếp xúc với các câu trả lời tuyệt vời được đăng ở đây.
Michał Marc:

... tốt, đối với Clojure, ít nhất một lý do bổ sung được áp dụng: Bạn mất khả năng tương thích với ClojureScript và các dẫn xuất của nó.
Charles Duffy

Câu trả lời:


148

Có một số lý do tại sao một người không nên sử dụng EVAL .

Lý do chính cho người mới bắt đầu là: bạn không cần nó.

Ví dụ (giả sử Lisp chung):

ĐÁNH GIÁ một biểu thức với các toán tử khác nhau:

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

Điều đó tốt hơn được viết là:

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

Có rất nhiều ví dụ mà người mới bắt đầu học Lisp nghĩ rằng họ cần EVAL, nhưng họ không cần nó - vì các biểu thức được đánh giá và người ta cũng có thể đánh giá phần chức năng. Hầu hết thời gian sử dụngEVAL cho thấy sự thiếu hiểu biết của người đánh giá.

Đây là vấn đề tương tự với các macro. Thông thường người mới bắt đầu viết macro, nơi họ nên viết các hàm - không hiểu macro thực sự là gì và không hiểu rằng một hàm đã thực hiện công việc.

Nó thường là công cụ sai cho công việc sử dụng EVAL và nó thường chỉ ra rằng người mới bắt đầu không hiểu các quy tắc đánh giá Lisp thông thường.

Nếu bạn nghĩ rằng bạn cần EVAL, sau đó kiểm tra nếu một cái gì đó như FUNCALL, REDUCEhoặc APPLYcó thể được sử dụng thay thế.

  • FUNCALL - gọi một hàm với các đối số: (funcall '+ 1 2 3)
  • REDUCE - gọi một hàm trên danh sách các giá trị và kết hợp các kết quả: (reduce '+ '(1 2 3))
  • APPLY- gọi một hàm với một danh sách là các đối số : (apply '+ '(1 2 3)).

Q: Tôi thực sự cần eval hay trình biên dịch / đánh giá đã là những gì tôi thực sự muốn?

Những lý do chính cần tránh EVALđối với người dùng cao cấp hơn một chút:

  • bạn muốn chắc chắn rằng mã của bạn được biên dịch, bởi vì trình biên dịch có thể kiểm tra mã cho nhiều vấn đề và tạo mã nhanh hơn, đôi khi RẤT NHIỀU (đó là yếu tố 1000 ;-)) mã nhanh hơn

  • mã được xây dựng và cần được đánh giá không thể được biên dịch sớm nhất có thể.

  • eval của đầu vào người dùng tùy ý mở ra các vấn đề bảo mật

  • một số sử dụng đánh giá có EVALthể xảy ra không đúng lúc và tạo ra các vấn đề xây dựng

Để giải thích điểm cuối cùng với một ví dụ đơn giản:

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

Vì vậy, tôi có thể muốn viết một macro dựa trên tham số đầu tiên sử dụng SINhoặcCOS .

(foo 3 4)làm (sin 4)(foo 1 4)làm(cos 4) .

Bây giờ chúng ta có thể có:

(foo (+ 2 1) 4)

Điều này không cho kết quả mong muốn.

Người ta có thể muốn sửa chữa macro FOObằng cách ĐÁNH GIÁ biến:

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

(foo (+ 2 1) 4)

Nhưng sau đó, điều này vẫn không hoạt động:

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

Giá trị của biến chỉ là không biết tại thời điểm biên dịch.

Một lý do quan trọng chung để tránh EVAL: nó thường được sử dụng cho các hack xấu xí.


3
Cảm ơn! Tôi chỉ không hiểu điểm cuối cùng (đánh giá không đúng lúc?) - bạn có thể nói rõ hơn một chút không?
Jay

41
+1 vì đây là câu trả lời thực sự - mọi người rơi vào evalđơn giản vì họ không biết có một tính năng thư viện hoặc ngôn ngữ cụ thể để làm những gì họ muốn làm. Ví dụ tương tự từ JS: Tôi muốn lấy một thuộc tính từ một đối tượng bằng cách sử dụng tên động, vì vậy tôi viết: eval("obj.+" + propName)khi tôi có thể đã viết obj[propName].
Daniel Earwicker

Tôi hiểu ý của bạn bây giờ, Rainer! Phạn!
Jay

@Daniel "obj.+":? Tôi đã kiểm tra lần cuối, +không hợp lệ khi sử dụng tham chiếu dấu chấm trong JS.
Xin chào71

2
@Daniel có lẽ có nghĩa là eval ("obj." + PropName) sẽ hoạt động như mong đợi.
claj

41

eval(trong bất kỳ ngôn ngữ nào) không phải là xấu theo cùng một cách mà cưa máy không phải là xấu xa. Nó là một công cụ. Nó tình cờ là một công cụ mạnh mẽ, khi bị lạm dụng, có thể cắt đứt chân tay và lở loét (nói theo nghĩa bóng), nhưng điều tương tự cũng có thể nói đối với nhiều công cụ trong hộp công cụ của lập trình viên bao gồm:

  • goto và những người bạn
  • khóa dựa trên luồng
  • tiếp tục
  • macro (lai hoặc khác)
  • con trỏ
  • ngoại lệ có thể khởi động lại
  • mã tự sửa đổi
  • ... và hàng ngàn diễn viên.

Nếu bạn thấy mình phải sử dụng bất kỳ công cụ mạnh mẽ, nguy hiểm tiềm tàng nào, hãy tự hỏi mình ba lần "tại sao?" trong một chuỗi Ví dụ:

"Tại sao tôi phải sử dụng eval?" "Vì foo." "Tại sao foo cần thiết?" "Bởi vì ..."

Nếu bạn đi đến cuối chuỗi đó và công cụ vẫn có vẻ như đó là điều đúng đắn, thì hãy làm điều đó. Tài liệu về địa ngục của nó. Kiểm tra địa ngục ra khỏi nó. Kiểm tra lại tính chính xác và bảo mật nhiều lần. Nhưng làm đi.


Cảm ơn - đó là những gì tôi đã nghe về eval trước đây ("tự hỏi tại sao"), nhưng tôi chưa bao giờ nghe hoặc đọc những vấn đề tiềm ẩn là gì. Bây giờ tôi thấy từ các câu trả lời ở đây chúng là gì (vấn đề bảo mật và hiệu suất).
Jay

8
Và mã dễ đọc. Eval hoàn toàn có thể làm hỏng dòng mã và khiến nó không thể hiểu được.
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI đúng

Tôi không hiểu tại sao "phân luồng dựa trên khóa" [sic] có trong danh sách của bạn. Có những hình thức đồng thời không liên quan đến khóa và vấn đề với khóa thường được biết đến, nhưng tôi chưa bao giờ nghe ai mô tả sử dụng khóa là "xấu xa".
asveikau

4
asveikau: Luồng dựa trên khóa rất khó để lấy đúng (tôi đoán rằng 99,44% mã sản xuất sử dụng khóa là xấu). Nó không sáng tác. Nó có xu hướng biến mã "đa luồng" của bạn thành mã nối tiếp. (Sửa lỗi cho điều này chỉ làm cho mã chậm và cồng kềnh thay vào đó.) Có những lựa chọn thay thế tốt cho luồng dựa trên khóa, như STM hoặc mô hình diễn viên, sử dụng mã này trong bất kỳ thứ gì ngoại trừ mã cấp thấp nhất.
CHỈ CẦN HOẠT ĐỘNG CỦA TÔI NGÀY

"tại sao chuỗi" :) hãy chắc chắn dừng lại sau 3 bước nó có thể bị tổn thương.
szymanowski

27

Eval vẫn ổn, miễn là bạn biết CHÍNH XÁC những gì đang diễn ra. Bất kỳ đầu vào của người dùng đi vào nó PHẢI được kiểm tra và xác nhận và mọi thứ. Nếu bạn không biết làm thế nào để chắc chắn 100%, thì đừng làm điều đó.

Về cơ bản, người dùng có thể nhập bất kỳ mã nào cho ngôn ngữ được đề cập và nó sẽ thực thi. Bạn có thể tưởng tượng cho mình bao nhiêu thiệt hại anh ta có thể làm.


1
Vì vậy, nếu tôi thực sự tạo biểu thức S dựa trên đầu vào của người dùng bằng thuật toán sẽ không sao chép trực tiếp đầu vào của người dùng và nếu điều đó dễ dàng và rõ ràng hơn trong một số trường hợp cụ thể hơn là sử dụng macro hoặc các kỹ thuật khác, thì tôi cho rằng không có gì "xấu xa " về nó? Nói cách khác, các vấn đề duy nhất với eval có giống với các truy vấn SQL và các kỹ thuật khác sử dụng đầu vào của người dùng trực tiếp không?
Jay

10
Lý do nó được gọi là "xấu xa" là bởi vì làm sai nó tệ hơn nhiều so với làm những việc khác sai. Và như chúng ta biết, người mới sẽ làm những thứ sai.
Tor Valamo

3
Tôi sẽ không nói rằng mã phải được xác nhận trước khi trốn tránh nó trong mọi trường hợp. Ví dụ, khi triển khai REPL đơn giản, có thể bạn chỉ cần đưa đầu vào vào eval không được kiểm tra và đó sẽ không phải là vấn đề (tất nhiên khi viết REPL dựa trên web, bạn cần một hộp cát, nhưng đó không phải là trường hợp bình thường CLI-REPL chạy trên hệ thống của người dùng).
sepp2k

1
Như tôi đã nói, bạn phải biết chính xác những gì xảy ra khi bạn cho ăn những gì bạn cho vào eval. Nếu điều đó có nghĩa là "nó sẽ thực thi một số lệnh trong giới hạn của hộp cát", thì đó là ý nghĩa của nó. ;)
Tor Valamo

@TorValamo bao giờ nghe nói về việc vượt ngục?
Loïc Faure-Lacroix

21

"Khi nào tôi nên sử dụng eval?" có thể là một câu hỏi tốt hơn

Câu trả lời ngắn gọn là "khi chương trình của bạn dự định viết một chương trình khác trong thời gian chạy, và sau đó chạy nó". Lập trình di truyền là một ví dụ về một tình huống mà nó có thể có ý nghĩa để sử dụng eval.


14

IMO, câu hỏi này không dành riêng cho LISP . Đây là một câu trả lời cho cùng một câu hỏi cho PHP và nó áp dụng cho LISP, Ruby và các ngôn ngữ khác có eval:

Các vấn đề chính với eval () là:

  • Đầu vào không an toàn tiềm năng. Vượt qua một tham số không đáng tin cậy là một cách để thất bại. Nó thường không phải là một nhiệm vụ tầm thường để đảm bảo rằng một tham số (hoặc một phần của nó) được tin cậy hoàn toàn.
  • Lừa đảo. Sử dụng eval () làm cho mã thông minh, do đó khó theo dõi hơn. Để trích dẫn Brian Kernighan " Gỡ lỗi khó gấp đôi so với viết mã ở vị trí đầu tiên. Do đó, nếu bạn viết mã càng khéo léo càng tốt, theo định nghĩa, bạn không đủ thông minh để gỡ lỗi "

Vấn đề chính với việc sử dụng eval () thực tế chỉ là một:

  • nhà phát triển thiếu kinh nghiệm sử dụng nó mà không xem xét đủ.

Lấy từ đây .

Tôi nghĩ rằng phần khó khăn là một điểm tuyệt vời. Nỗi ám ảnh về mã golf và mã ngắn gọn luôn dẫn đến mã "thông minh" (mà evals là một công cụ tuyệt vời). Nhưng bạn nên viết mã của mình để dễ đọc, IMO, không phải để chứng minh rằng bạn là một người thông minh và không tiết kiệm giấy (dù sao bạn cũng sẽ không in nó).

Sau đó, trong LISP có một số vấn đề liên quan đến bối cảnh chạy eval, vì vậy mã không tin cậy có thể có quyền truy cập vào nhiều thứ hơn; vấn đề này dường như là phổ biến


3
Vấn đề "đầu vào xấu" với EVAL chỉ ảnh hưởng đến các ngôn ngữ không phải là Lisp, bởi vì trong các ngôn ngữ đó, eval () thường có một đối số chuỗi và đầu vào của người dùng thường được ghép vào. Người dùng có thể đưa vào một trích dẫn trong đầu vào của họ và thoát vào mã được tạo. Nhưng trong Lisp, đối số của EVAL không phải là một chuỗi và đầu vào của người dùng không thể thoát vào mã trừ khi bạn hoàn toàn liều lĩnh (như bạn đã phân tích cú pháp đầu vào bằng READ-FROM-STRING để tạo biểu thức S, sau đó bạn đưa vào mã EVAL mà không trích dẫn nó. Nếu bạn trích dẫn nó, không có cách nào để thoát khỏi trích dẫn).
Vứt bỏ tài khoản

12

Đã có nhiều câu trả lời tuyệt vời nhưng đây là một câu hỏi khác của Matthew Flatt, một trong những người thực hiện vợt:

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

Anh ta đưa ra nhiều điểm đã được bảo hiểm nhưng một số người có thể thấy anh ta rất thú vị.

Tóm tắt: Bối cảnh sử dụng nó ảnh hưởng đến kết quả của eval nhưng thường không được các lập trình viên xem xét, dẫn đến kết quả không mong muốn.


11

Câu trả lời kinh điển là tránh xa. Điều mà tôi thấy kỳ lạ, bởi vì đó là một người nguyên thủy, và trong bảy người nguyên thủy (những người khác là khuyết điểm, xe hơi, cdr, nếu, eq và trích dẫn), nó ngày càng ít sử dụng và yêu thích.

Từ trên Lisp : "Thông thường, gọi eval rõ ràng giống như mua một thứ gì đó trong cửa hàng quà tặng ở sân bay. Phải đợi đến giây phút cuối cùng, bạn phải trả giá cao cho một lựa chọn hạn chế của hàng hóa hạng hai."

Vậy khi nào tôi sử dụng eval? Một cách sử dụng thông thường là có REPL trong REPL của bạn bằng cách đánh giá (loop (print (eval (read)))). Mọi người đều ổn với việc sử dụng đó.

Nhưng bạn cũng có thể định nghĩa các hàm theo các macro sẽ được đánh giá sau khi biên dịch bằng cách kết hợp eval với backquote. Anh đi

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

và nó sẽ giết chết bối cảnh cho bạn.

Swank (cho emacs slime) có đầy đủ các trường hợp này. Họ trông như thế này:

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

Tôi không nghĩ đó là một hack bẩn thỉu. Tôi sử dụng tất cả thời gian bản thân để tái hòa nhập các macro vào các chức năng.


1
Bạn có thể muốn kiểm tra ngôn ngữ kernel;)
artemonster

7

Một vài điểm khác trên Lisp eval:

  • Nó đánh giá dưới môi trường toàn cầu, mất bối cảnh địa phương của bạn.
  • Đôi khi bạn có thể muốn sử dụng eval, khi bạn thực sự có ý định sử dụng macro đọc '#.' mà đánh giá tại thời điểm đọc.

Tôi hiểu rằng việc sử dụng env toàn cầu là đúng cho cả Common Lisp và Scheme; nó cũng đúng với Clojure?
Jay

2
Trong Đề án (ít nhất là đối với R7RS, có lẽ đối với R6RS), bạn phải vượt qua một môi trường để đánh bại.
csl

4

Giống như "quy tắc" của GOTO: Nếu bạn không biết những gì bạn đang làm, bạn có thể tạo ra một mớ hỗn độn.

Ngoài việc chỉ xây dựng một cái gì đó từ dữ liệu đã biết và an toàn, có một vấn đề là một số ngôn ngữ / cách triển khai không thể tối ưu hóa đủ mã. Bạn có thể kết thúc với mã được giải thích bên trong eval.


Quy tắc đó có liên quan gì với GOTO? Có tính năng nào trong bất kỳ ngôn ngữ lập trình nào mà bạn không thể tạo ra một mớ hỗn độn không?
Ken

2
@Ken: Không có quy tắc GOTO, do đó dấu ngoặc kép trong câu trả lời của tôi. Chỉ có một giáo điều cho những người sợ nghĩ cho chính họ. Tương tự cho eval. Tôi nhớ tăng tốc đáng kể một số kịch bản Perl bằng cách sử dụng eval. Đây là một công cụ trong hộp công cụ của bạn. Người mới thường sử dụng eval khi các cấu trúc ngôn ngữ khác dễ dàng hơn / tốt hơn. Nhưng tránh nó hoàn toàn chỉ để được mát mẻ và làm hài lòng những người giáo điều?
stesch

4

Eval chỉ là không an toàn. Ví dụ: bạn có đoạn mã sau:

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

Bây giờ người dùng đến trang web của bạn và nhập url http://example.com/file.php?user= ); $ is_admin = true; echo (

Sau đó, mã kết quả sẽ là:

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

6
anh ta đang nói về Lisp nghĩ không phải php
fmsf

4
@fmsf Anh ấy đã nói cụ thể về Lisp nhưng nói chung về evalbất kỳ ngôn ngữ nào có nó.
Skilldrick

4
@fmsf - đây thực sự là một câu hỏi độc lập với ngôn ngữ. Nó thậm chí còn áp dụng cho các ngôn ngữ được biên dịch tĩnh vì chúng có thể mô phỏng eval bằng cách gọi ra trình biên dịch khi chạy.
Daniel Earwicker

1
trong trường hợp đó, ngôn ngữ là một bản sao. Tôi đã thấy rất nhiều như thế này ở đây.
fmsf

9
PHP eval không giống như Lisp eval. Hãy nhìn xem, nó hoạt động trên một chuỗi ký tự và việc khai thác trong URL phụ thuộc vào việc có thể đóng dấu ngoặc đơn văn bản và mở một chuỗi ký tự khác. Lisp eval không nhạy cảm với loại công cụ này. Bạn có thể đánh giá dữ liệu dưới dạng đầu vào từ một mạng, nếu bạn đặt hộp cát đúng cách (và cấu trúc đủ dễ để đi bộ để làm điều đó).
Kaz

2

Eval không xấu xa. Eval không phức tạp. Nó là một hàm biên dịch danh sách bạn truyền cho nó. Trong hầu hết các ngôn ngữ khác, biên dịch mã tùy ý có nghĩa là học AST của ngôn ngữ và đào sâu vào bên trong trình biên dịch để tìm ra API trình biên dịch. Trong lisp, bạn chỉ cần gọi eval.

Khi nào bạn nên sử dụng nó? Bất cứ khi nào bạn cần biên dịch một cái gì đó, điển hình là một chương trình chấp nhận, tạo hoặc sửa đổi mã tùy ý khi chạy .

Khi nào bạn không nên sử dụng nó? Tất cả các trường hợp khác.

Tại sao bạn không nên sử dụng nó khi bạn không cần? Bởi vì bạn sẽ làm một cái gì đó theo một cách phức tạp không cần thiết có thể gây ra vấn đề về khả năng đọc, hiệu suất và gỡ lỗi.

Vâng, nhưng nếu tôi là người mới bắt đầu, làm sao tôi biết tôi có nên sử dụng nó không? Luôn cố gắng thực hiện những gì bạn cần với các chức năng. Nếu điều đó không làm việc, thêm macro. Nếu điều đó vẫn không hiệu quả, thì eval!

Thực hiện theo các quy tắc này và bạn sẽ không bao giờ làm điều ác với eval :)


0

Tôi rất thích câu trả lời của Zak và anh ấy đã hiểu được cốt lõi của vấn đề: eval được sử dụng khi bạn đang viết một ngôn ngữ mới, một kịch bản hoặc sửa đổi ngôn ngữ. Anh ấy không thực sự giải thích thêm vì vậy tôi sẽ đưa ra một ví dụ:

(eval (read-line))

Trong chương trình Lisp đơn giản này, người dùng được nhắc nhập liệu và sau đó bất cứ điều gì họ nhập vào đều được đánh giá. Để làm việc này toàn bộ tập hợp các định nghĩa ký hiệu phải có mặt nếu chương trình được biên dịch, bởi vì bạn không biết người dùng có thể nhập chức năng nào, vì vậy bạn phải bao gồm tất cả chúng. Điều đó có nghĩa là nếu bạn biên dịch chương trình đơn giản này, nhị phân kết quả sẽ là khổng lồ.

Theo nguyên tắc, bạn thậm chí không thể coi đây là một tuyên bố có thể biên dịch được vì lý do này. Nói chung, một khi bạn sử dụng eval , bạn đang hoạt động trong một môi trường được giải thích và mã không còn có thể được biên dịch. Nếu bạn không sử dụng eval thì bạn có thể biên dịch chương trình Lisp hoặc Scheme giống như chương trình C. Do đó, bạn muốn chắc chắn rằng bạn muốn và cần phải ở trong một môi trường được giải thích trước khi cam kết sử dụng eval .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.