Tại sao phong cách xấu để 'giải cứu Ngoại lệ => e` trong Ruby?


895

Ruby QuickRef của Ryan Davis nói (không cần giải thích):

Đừng giải cứu Ngoại lệ. KHÔNG BAO GIỜ. hoặc tôi sẽ đâm bạn.

Tại sao không? Điều gì là đúng để làm gì?


35
Sau đó, bạn có thể có thể viết của riêng bạn? :)
Sergio Tulentsev

65
Tôi rất khó chịu với lời kêu gọi bạo lực ở đây. Đó chỉ là lập trình.
Darth Egregious

1
Hãy xem bài viết này trong Ruby Exception với hệ thống phân cấp Ruby Exception đẹp .
Atul Khanduri

2
Bởi vì Ryan Davis sẽ đâm bạn. Vì vậy, những đứa trẻ. Không bao giờ giải cứu Ngoại lệ.
Mugen

7
@DarthEgregious Tôi thực sự không thể biết bạn có đùa hay không. Nhưng tôi nghĩ nó rất vui nhộn. (Và rõ ràng nó không phải là một mối đe dọa nghiêm trọng). Bây giờ mỗi khi tôi nghĩ về việc bắt Exception, tôi sẽ cân nhắc liệu có đáng bị đâm bởi một kẻ ngẫu nhiên nào đó trên Internet không.
Steve Sether

Câu trả lời:


1375

TL; DR : Sử dụng StandardErrorthay thế cho việc bắt ngoại lệ chung. Khi ngoại lệ ban đầu được nâng lại (ví dụ: khi giải cứu để chỉ ghi nhật ký ngoại lệ), việc giải cứu Exceptioncó thể là ổn.


Exceptionlà gốc rễ của hệ thống phân cấp ngoại lệ Ruby , vì vậy khi bạn rescue Exceptionđã giải thoát khỏi tất cả mọi thứ , bao gồm cả các lớp con như SyntaxError, LoadError, và Interrupt.

Việc giải cứu Interruptngăn người dùng sử dụng CTRLCđể thoát khỏi chương trình.

Việc giải cứu SignalExceptionngăn chương trình phản hồi chính xác với tín hiệu. Nó sẽ không thành công trừ khi kill -9.

Giải cứu SyntaxErrorcó nghĩa là evalthất bại sẽ âm thầm như vậy.

Tất cả những thứ này có thể được hiển thị bằng cách chạy chương trình này và cố gắng CTRLChoặc killnó:

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

Giải cứu từ Exceptionthậm chí không phải là mặc định. Đang làm

begin
  # iceberg!
rescue
  # lifeboats
end

không giải cứu từ Exception, nó giải cứu từ StandardError. Nói chung, bạn nên chỉ định một cái gì đó cụ thể hơn mặc định StandardError, nhưng việc giải cứu khỏi việc Exception mở rộng phạm vi thay vì thu hẹp phạm vi và có thể có kết quả thảm khốc và khiến việc săn lỗi trở nên vô cùng khó khăn.


Nếu bạn có một tình huống mà bạn muốn giải cứu StandardErrorvà bạn cần một biến có ngoại lệ, bạn có thể sử dụng biểu mẫu này:

begin
  # iceberg!
rescue => e
  # lifeboats
end

tương đương với:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

Một trong số ít các trường hợp phổ biến mà việc giải cứu Exceptionnó là vì mục đích ghi nhật ký / báo cáo, trong trường hợp đó bạn nên ngay lập tức nêu lại ngoại lệ:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end

129
vì vậy, nó giống như bắt Throwabletrong java
ratchet freak

53
Lời khuyên này tốt cho môi trường Ruby sạch. Nhưng thật không may, một số đá quý đã tạo ra các ngoại lệ trực tiếp từ Exception. Môi trường của chúng tôi có 30 trong số này: ví dụ OpenID :: Server :: EncodingError, OAuth :: UnlimitedRequest, HTMLTokenizerSample. Đây là những trường hợp ngoại lệ mà bạn rất muốn bắt trong các khối cứu hộ tiêu chuẩn. Thật không may, không có gì trong Ruby ngăn cản hoặc thậm chí ngăn cản các viên đá quý kế thừa trực tiếp từ Ngoại lệ - ngay cả việc đặt tên cũng không trực quan.
Jonathan Swartz

20
@JonathanSwartz Sau đó giải cứu khỏi các lớp con cụ thể đó, không phải Ngoại lệ. Cụ thể hơn là gần như luôn luôn tốt hơn và rõ ràng hơn.
Andrew Marshall

22
@JonathanSwartz - Tôi sẽ yêu cầu những người tạo đá quý thay đổi những gì ngoại lệ của họ thừa hưởng từ đó. Cá nhân, tôi thích đá quý của tôi có tất cả các ngoại lệ xuất phát từ MyGemException, vì vậy bạn có thể giải cứu nó nếu bạn muốn.
Nathan Long

12
Bạn cũng có thể ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]và sau đórescue *ADAPTER_ERRORS => e
j_mcnally

83

Nguyên tắc thực sự là: Đừng vứt bỏ ngoại lệ. Tính khách quan của tác giả trích dẫn của bạn là nghi vấn, bằng chứng là nó kết thúc bằng

hoặc tôi sẽ đâm bạn

Tất nhiên, lưu ý rằng các tín hiệu (theo mặc định) đưa ra các ngoại lệ và thông thường các quy trình chạy dài bị chấm dứt thông qua tín hiệu, do đó, việc bắt Ngoại lệ và không chấm dứt ngoại lệ tín hiệu sẽ khiến chương trình của bạn rất khó dừng lại. Vì vậy, đừng làm điều này:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

Không, thực sự, đừng làm điều đó. Đừng chạy nó để xem nó có hoạt động không.

Tuy nhiên, giả sử bạn có một máy chủ luồng và bạn muốn tất cả các ngoại lệ không:

  1. bị bỏ qua (mặc định)
  2. dừng máy chủ (điều này xảy ra nếu bạn nói thread.abort_on_exception = true).

Sau đó, điều này là hoàn toàn chấp nhận được trong luồng xử lý kết nối của bạn:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

Ở trên có một biến thể của trình xử lý ngoại lệ mặc định của Ruby, với ưu điểm là nó cũng không giết chết chương trình của bạn. Rails thực hiện điều này trong trình xử lý yêu cầu của nó.

Ngoại lệ tín hiệu được đưa ra trong chủ đề chính. Chủ đề nền sẽ không có được chúng, vì vậy không có lý do gì để cố bắt chúng ở đó.

Điều này đặc biệt hữu ích trong môi trường sản xuất, nơi bạn không muốn chương trình của mình đơn giản dừng lại mỗi khi có sự cố. Sau đó, bạn có thể lấy các ngăn xếp ngăn xếp trong nhật ký của bạn và thêm vào mã của bạn để xử lý ngoại lệ cụ thể tiếp tục xuống chuỗi cuộc gọi và theo cách duyên dáng hơn.

Cũng lưu ý rằng có một thành ngữ Ruby khác cũng có tác dụng tương tự:

a = do_something rescue "something else"

Trong dòng này, nếu do_somethingđưa ra một ngoại lệ, nó bị Ruby bắt, vứt đi và ađược chỉ định "something else".

Nói chung, đừng làm điều đó, trừ những trường hợp đặc biệt mà bạn biết bạn không cần phải lo lắng. Một ví dụ:

debugger rescue nil

Các debuggerchức năng là một cách khá tốt đẹp để thiết lập một breakpoint trong mã của bạn, nhưng nếu chạy bên ngoài một trình gỡ lỗi, và Rails, nó đặt ra một ngoại lệ. Bây giờ về mặt lý thuyết, bạn không nên để mã gỡ lỗi nằm trong chương trình của mình (pff! Không ai làm thế!) Nhưng bạn có thể muốn giữ nó ở đó một lúc vì một số lý do, nhưng không tiếp tục chạy trình gỡ lỗi của bạn.

Ghi chú:

  1. Nếu bạn đã chạy chương trình của người khác bắt các ngoại lệ tín hiệu và bỏ qua chúng, (giả sử mã ở trên) thì:

    • trong Linux, trong một trình bao, gõ pgrep ruby, hoặc ps | grep ruby, tìm kiếm bộ vi phạm chương trình vi phạm của bạn, rồi chạy kill -9 <PID>.
    • trong Windows, sử dụng Trình quản lý tác vụ ( CTRL- SHIFT- ESC), chuyển đến tab "quy trình", tìm quy trình của bạn, nhấp chuột phải vào nó và chọn "Kết thúc quá trình".
  2. Nếu bạn đang làm việc với chương trình của người khác, vì bất kỳ lý do gì, bị ảnh hưởng bởi các khối ngoại lệ bỏ qua này, thì việc đặt chương trình này ở đầu dòng chính là một khả năng đồng bộ:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }

    Điều này khiến chương trình phản hồi các tín hiệu kết thúc bình thường bằng cách chấm dứt ngay lập tức, bỏ qua các trình xử lý ngoại lệ, không dọn dẹp . Vì vậy, nó có thể gây mất dữ liệu hoặc tương tự. Hãy cẩn thận!

  3. Nếu bạn cần làm điều này:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    bạn thực sự có thể làm điều này:

    begin
      do_something
    ensure
      critical_cleanup
    end

    Trong trường hợp thứ hai, critical cleanupsẽ được gọi mỗi lần, cho dù có ném ngoại lệ hay không.


21
Xin lỗi, điều này là sai. Một máy chủ không bao giờ nên cứu Exception và không làm gì ngoài việc đăng nhập nó. Điều đó sẽ làm cho nó không thành công ngoại trừ kill -9.
Giăng

8
Các ví dụ của bạn trong ghi chú 3 không tương đương, ensuresẽ chạy bất kể có ngoại lệ nào được nêu ra hay không, trong khi ý rescuechí chỉ chạy nếu một ngoại lệ được nêu ra.
Andrew Marshall

1
Chúng không / chính xác / tương đương nhưng tôi không thể tìm ra cách diễn đạt ngắn gọn sự tương đương theo cách không xấu.
Michael Slade

3
Chỉ cần thêm một lệnh gọi_cleanup khác sau khối bắt đầu / cứu trong ví dụ đầu tiên. Tôi đồng ý không phải là mã thanh lịch nhất, nhưng rõ ràng ví dụ thứ hai là cách làm thanh lịch, vì vậy một chút không liên quan chỉ là một phần của ví dụ.
gtd

3
"Đừng chạy nó để xem nó có hoạt động không." Có vẻ là một lời khuyên tồi cho việc viết mã ... Ngược lại, tôi sẽ khuyên bạn nên chạy nó, để xem nó thất bại và tự mình hiểu làm thế nào nếu thất bại, thay vì tin tưởng một cách mù quáng vào người khác. Câu trả lời tuyệt vời nào :)
huelbois

69

TL; DR

Đừng rescue Exception => e(và không nêu lại ngoại lệ) - hoặc bạn có thể lái xe khỏi cầu.


Giả sử bạn đang ở trong xe (chạy Ruby). Gần đây, bạn đã cài đặt một tay lái mới với hệ thống nâng cấp không dây (sử dụng eval), nhưng bạn không biết một trong những lập trình viên đã nhầm lẫn về cú pháp.

Bạn đang ở trên một cây cầu, và nhận ra rằng bạn đang đi một chút về phía lan can, vì vậy bạn rẽ trái.

def turn_left
  self.turn left:
end

Giáo sư! Điều đó có lẽ không tốt ™, may mắn thay, Ruby tăng a SyntaxError.

Xe nên dừng ngay - phải không?

Không.

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

bíp bíp

Cảnh báo: Bị bắt ngoại lệ SyntaxError.

Thông tin: Lỗi đăng nhập - Quá trình tiếp tục.

Bạn nhận thấy một cái gì đó là sai, và bạn slam trên phá vỡ tình trạng khẩn cấp ( ^C: Interrupt)

bíp bíp

Cảnh báo: Bị bắt ngoại lệ.

Thông tin: Lỗi đăng nhập - Quá trình tiếp tục.

Vâng - điều đó không giúp được gì nhiều. Bạn khá gần đường ray, vì vậy bạn đặt xe vào công viên ( killing SignalException:).

bíp bíp

Cảnh báo: Bắt tín hiệu ngoại lệ.

Thông tin: Lỗi đăng nhập - Quá trình tiếp tục.

Vào giây cuối cùng, bạn rút chìa khóa ( kill -9) và xe dừng lại, bạn đâm thẳng vào vô lăng (túi khí không thể phồng lên vì bạn đã không dừng chương trình một cách duyên dáng - bạn đã chấm dứt chương trình) và máy tính ở phía sau xe của bạn đâm vào chỗ ngồi phía trước nó. Một lon Coke đầy một nửa tràn ra giấy tờ. Các cửa hàng tạp hóa ở phía sau bị nghiền nát, và hầu hết được bao phủ trong lòng đỏ trứng và sữa. Chiếc xe cần sửa chữa và làm sạch nghiêm trọng. (Mất dữ liệu)

Hy vọng bạn có bảo hiểm (Sao lưu). Ồ vâng - vì túi khí không phồng lên, bạn có thể bị tổn thương (bị đuổi việc, v.v.).


Nhưng chờ đã! Cóhơnlý do tại sao bạn có thể muốn sử dụng rescue Exception => e!

Giả sử bạn là chiếc xe đó và bạn muốn chắc chắn rằng túi khí sẽ phồng lên nếu chiếc xe vượt quá đà dừng an toàn.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

Đây là ngoại lệ cho quy tắc: Bạn chỉ có thể bắt Exception nếu bạn nâng lại ngoại lệ . Vì vậy, một quy tắc tốt hơn là không bao giờ nuốt Exception, và luôn nêu lại lỗi.

Nhưng việc thêm giải cứu vừa dễ quên trong một ngôn ngữ như Ruby, vừa đưa ra tuyên bố giải cứu ngay trước khi nêu lại một vấn đề cảm thấy hơi KHÔNG KHÔ. Và bạn không muốn quên raisecâu nói. Và nếu bạn làm, chúc may mắn tìm thấy lỗi đó.

Rất may, Ruby thật tuyệt vời, bạn chỉ cần sử dụng ensuretừ khóa, đảm bảo mã sẽ chạy. Các ensuretừ khóa sẽ chạy mã không có vấn đề gì - nếu một ngoại lệ được ném ra, nếu người ta không là ngoại lệ duy nhất là nếu thế giới kết thúc (hoặc các sự kiện không khác).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

Bùng nổ! Và mã đó nên chạy bằng mọi cách. Lý do duy nhất bạn nên sử dụng rescue Exception => elà nếu bạn cần quyền truy cập vào ngoại lệ hoặc nếu bạn chỉ muốn mã chạy trên một ngoại lệ. Và nhớ nâng lại lỗi. Mỗi lần.

Lưu ý: Như @Niall đã chỉ ra, đảm bảo luôn chạy. Điều này là tốt bởi vì đôi khi chương trình của bạn có thể nói dối bạn và không ném ngoại lệ, ngay cả khi vấn đề xảy ra. Với các nhiệm vụ quan trọng, như bơm hơi túi khí, bạn cần chắc chắn rằng nó xảy ra bất kể điều gì. Bởi vì điều này, kiểm tra mỗi khi xe dừng lại, liệu có ngoại lệ được ném hay không, là một ý tưởng tốt. Mặc dù bơm hơi là một nhiệm vụ không phổ biến trong hầu hết các bối cảnh lập trình, nhưng điều này thực sự khá phổ biến với hầu hết các nhiệm vụ dọn dẹp.


12
Hahahaha! Đây là một câu trả lời tuyệt vời. Tôi sốc vì không ai bình luận. Bạn đưa ra một kịch bản rõ ràng làm cho toàn bộ điều thực sự dễ hiểu. Chúc mừng! :-)
James Milani

@JamesMilani Cảm ơn bạn!
Ben Aubin

3
+ Cho câu trả lời này. Chúc tôi có thể nâng cao hơn một lần!
kỹ

1
Rất thích câu trả lời của bạn!
Atul Vaibhav

3
Câu trả lời này được đưa ra 4 năm sau câu trả lời được chấp nhận hoàn toàn dễ hiểu và chính xác, và giải thích lại nó với một kịch bản ngớ ngẩn được thiết kế để gây cười hơn là thực tế. Xin lỗi để trở thành một buzzkill, nhưng điều này không phải là reddit - điều quan trọng hơn là câu trả lời ngắn gọn và chính xác hơn là hài hước. Ngoài ra, phần ensurethay thế rescue Exceptionlà sai lệch - ví dụ ngụ ý rằng chúng tương đương nhau, nhưng như đã nêu ensuresẽ xảy ra cho dù có Ngoại lệ hay không, vì vậy bây giờ túi khí của bạn sẽ phồng lên vì bạn đã vượt quá 5mph mặc dù không có gì sai.
Niall

47

Bởi vì điều này nắm bắt tất cả các ngoại lệ. Không chắc là chương trình của bạn có thể phục hồi từ bất kỳ ai trong số họ.

Bạn chỉ nên xử lý các trường hợp ngoại lệ mà bạn biết cách phục hồi. Nếu bạn không lường trước được một loại ngoại lệ nào đó, đừng xử lý nó, hãy nói to (viết chi tiết vào nhật ký), sau đó chẩn đoán nhật ký và sửa mã.

Nuốt ngoại lệ là xấu, đừng làm điều này.


10

Đó là một trường hợp cụ thể của quy tắc mà bạn không nên bắt gặp bất kỳ ngoại lệ nào mà bạn không biết cách xử lý. Nếu bạn không biết cách xử lý nó, tốt hơn hết là hãy để một phần khác của hệ thống nắm bắt và xử lý nó.


0

Tôi vừa đọc một bài đăng blog tuyệt vời về nó tại honeybadger.io :

Ruby's Exception vs StandardError: Sự khác biệt là gì?

Tại sao bạn không nên giải cứu Ngoại lệ

Vấn đề với việc giải cứu Ngoại lệ là nó thực sự giải cứu mọi ngoại lệ thừa hưởng từ Ngoại lệ. Đó là .... tất cả bọn họ!

Đó là một vấn đề vì có một số trường hợp ngoại lệ được Ruby sử dụng nội bộ. Họ không có bất cứ điều gì để làm với ứng dụng của bạn và nuốt chúng sẽ khiến những điều tồi tệ xảy ra.

Dưới đây là một vài trong số những người lớn:

  • SignalException :: Interrupt - Nếu bạn cứu điều này, bạn không thể thoát khỏi ứng dụng của mình bằng cách nhấn control-c.

  • ScriptError :: SyntaxError - Nuốt lỗi cú pháp có nghĩa là những thứ như put ("Quên một cái gì đó) sẽ âm thầm thất bại.

  • NoMemoryError - Bạn muốn biết điều gì sẽ xảy ra khi chương trình của bạn tiếp tục chạy sau khi nó sử dụng hết RAM? Tôi cũng không.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

Tôi đoán rằng bạn không thực sự muốn nuốt bất kỳ ngoại lệ nào ở cấp độ hệ thống này. Bạn chỉ muốn nắm bắt tất cả các lỗi cấp độ ứng dụng của bạn. Các ngoại lệ gây ra mã CỦA BẠN.

May mắn thay, có một cách dễ dàng để làm điều này.

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.