Xử lý Ngoại lệ trong Ruby
Xử lý Ngoại lệ trong Ruby 1. Giới thiệu Trong quá trình lập trình, Khi thực thi một Action luôn đi kèm một hoặc nhiều ngoại lệ. Ví dụ bạn muốn find một record trong table "posts" và truyền vào post_id, nếu như id này không tồn tại thì rails sẽ bắn ra một ngoại lệ "RecordNotFound". Nếu không ...
Xử lý Ngoại lệ trong Ruby
1. Giới thiệu
Trong quá trình lập trình, Khi thực thi một Action luôn đi kèm một hoặc nhiều ngoại lệ. Ví dụ bạn muốn find một record trong table "posts" và truyền vào post_id, nếu như id này không tồn tại thì rails sẽ bắn ra một ngoại lệ "RecordNotFound".
Nếu không xử lý được ngoại lệ này thì trang web của bạn sẽ "chết" mà không đưa ra bất cứ hướng dẫn và giải thích gì cho người dùng. Đó là là một điều tồi tệ.
Vì vậy, Xử lý ngoại lệ để điều hướng hoặc đưa ra một action khác để xử lý thay vì dừng trương trình một cách đột ngột là điều hết sức cần thiết.
Ruby cung cấp một cách để xử lý ngoại lệ tương đối dễ dàng. Đó là, Chúng ta sẽ bao bọc những đoạn code mà có thể gây ra được lỗi ở trong một khối begin/end và sử dụng rescue để cho Ruby biết được type của ngoại lệ mà khối lệnh có thể gặp phải. Tring ví dụ trên có thể là RecordNotFound.
2. Xử lý Ngoại lệ
a. Cú pháp
begin // khối lệnh được thực thi rescue // xử lý ngoại lệ ở đây end
Ví dụ:
#!/usr/bin/ruby begin file = open("not_exist_file.txt") if file puts "File opened successfully" end rescue puts "File not found" end print file, " "
Kết quả: Thay vì dừng khối lệnh ngay tại dòng code bị lỗi như sau:
No such file or directory @ rb_sysopen - file_name`.txt (Errno::ENOENT)
Thì chương trình sẽ đưa ra kết quả như người lập trình mong muốn:
File not found
b. Sử dụng lệnh retry
Sử dụng retry sẽ giúp bạn lặp lại việc thực hiện khối lệnh trên trong khối lệnh begin. Tuy nhiên, cần phải chú ý đến việc sử dụng lệnh này vì nó sẽ tạo ra vòng lặp vô hạn nếu bạn không handle nó tốt. Cú pháp
begin // khối lệnh được thực thi rescue // xử lý ngoại lệ ở đây retry // thực hiện lại code trong khối begin end
Ví du:
begin file = open("file_name1.txt") if file puts "File opened successfully" end rescue puts "File not found" retry end
Trong ví dụ trên các bạn sẽ thấy Ruby sẽ chạy khối lệnh trong begin và ngoại lệ FileNotFound nhưng sau đó sẽ quay lại chạy tiếp khi gặp lệnh retry - đây là một xử lý không tốt đúng không nào.
Trong từng trường hợp khác nhau mà bạn hãy sử dụng cho nó cho linh hoạt nhé.
Chẳng hạn như bạn có thể chỉ rõ số lần retry cho phép:
def retry_to_call(retry_times, &block) block.call rescue Exception => e if retry_times > 0 p 'gặp Ngoại lệ và retry lần: ' + retry_times.to_s retry_times -= 1 retry else p 'Hết số lần retry!' # raise e end end retry_to_call(3) do file = open("file_name`.txt") if file puts "File opened successfully" end end
Như vậy khối lệnh chỉ được retry lại retry_times lần:
hoangquan@hoangvanquan:~/data/reports/exeptions$ ruby retry.rb "gặp Ngoại lệ và retry lần: 3" "gặp Ngoại lệ và retry lần: 2" "gặp Ngoại lệ và retry lần: 1" "Hết số lần retry!"
c. Sử dụng lệnh raise
Raise là phương thức giúp bạn đưa ra một ngoại lệ. Bất cứ khi nào gặp lệnh raise thì chương trình của bạn sẽ ngay lập tức đưa ra Exception hiện tại (nếu không gặp ngoại lệ nào thì đưa ra RuntimeError).
Cú pháp:
raise // Đưa ra current Exception hoặc RuntimeError nếu không có ngoại lệ nào. Được sử dụng khi bạn muốn chặn trước một ngoại lệ nào đó mà biết biết chắc nó sẽ xảy ra ngay sau đó. raise "Error Message" // Đưa ra ngoại lệ kèm theo messages raise ExceptionType, "Error Message" // Đưa ra ngoại lệ được định dạng sẵn kiểu của ngoại lệ đó kèm theo messages raise ExceptionType, "Error Message" condition // Đưa ra ngoại lệ được định dạng sẵn kiểu của ngoại lệ đó kèm theo messages khi điều kiện nào đó xảy ra
Việc ngăn chặn trước ngoại lệ có thể xảy ra bằng cách raise ra một Ngoại lệ mà bạn chuẩn bị trước đó được thực hiện ngay trong khối lệnh begin, Tức là trong quá trình đoạn code của bạn được thực thi bạn đã lường trước được sẽ có trường hợp ngoại lệ xảy ra sau đó. Bạn cũng có thể định nghĩa riêng kiểu ngoại lệ mà bạn muốn.
Ví dụ:
module NgoaiLe class TenKhongTonTai < StandardError; end end begin name = nil raise NgoaiLe::TenKhongTonTai, "Tên không tồn tại " if name.nil? rescue Exception => e puts e.inspect end
Điều này khá hữu ích trong việc ghi lại logs cũng như dễ để người lập trình nắm bắt được lỗi xảy ra trong hệ thống.
Kết quả:
hoangquan@hoangvanquan:~/data/reports/exeptions$ ruby raise.rb #<NgoaiLe::TenKhongTonTai: Tên không tồn tại >
d. Sử dụng lệnh ensure và else
Đôi khi, Bạn cần đảm bảo một số hành động luôn luôn xảy ra cho dù có hay không có ngoại lệ nào xảy ra. ensure rất hữu ích trong trường hợp này.
Nếu có xuất hiện mệnh đề esle trong khối lệnh begin/end của bạn thì nó sẽ nằm sau khối lệnh rescuse và trước khối ensure (nếu có). Tất cả mã code trong Mệnh đề else sẽ chỉ được chạy khi không có bất kì ngoại lệ nào xảy ra.
Ví dụ
begin # raise 'Đưa ra lỗi bất kỳ' rescue Exception => e puts e.message puts e.backtrace.inspect else p "else - chạy khi Không có bất kỳ ngoại lệ nào" ensure puts "ensure - Tôi luôn chạy dù có bất kỳ ngoại lệ nào" end
e. Sử dụng khối Throw/catch
Cơ chế xử lý ngoại lệ khi sử dụng raise và rescure trong khối lệnh begin/end là từ bỏ thực thi nhiệm vụ khi gặp ngoại lệ. Tuy nhiên, trong một số trường hợp thì bạn chỉ cần bỏ qua (nhảy qua) nhiệm vụ đó và tiếp tục thực hiện chương trình thì sẽ tốt hơn. Đó là lý do Ruby cung cấp Throw/catch. Cách thức làm việc của Catch/Throw như sau: Định nghĩa một khối lệnh trong vùng Catch và gán tên cho nó.
Ví dụ:
floor = [["Một", "Hai", "Ba"], ["Bốn", "Năm", "Sáu"], ["Bảy", "Tám", "Chín"]] number = 0 tang = catch(:found) do floor.each do |row| row.each do |tile| number += 1 throw(:found, tile) if tile == "Năm" end end end puts tang puts number
Kết quả:
hoangquan@hoangvanquan:~/data/reports/exeptions$ ruby throw_catch.rb Năm 5
3. Class Exception
Trong Ruby Exception được chia thành nhiều loại thuộc các Class khác nhau nhưng đều là con của Class Exception.
Exception được chia thành bảy loại cơ bản: Interrupt NoMemoryError SignalException ScriptError StandardError SystemExit Trong đó ScriptError và StandardError là hai lớp có thêm các lớp con để chia nhỏ Ngoại lệ và xử lý.
Việc chia nhỏ ngoại lệ thành các Kiểu khác nhau và gom nhóm chúng vào một Class giúp tối ưu hiệu xuất cho chương trình của bạn.
Chú ý:
- Theo kinh nghiệm của mình các bạn tuyệt đối không nên xử dụng:
rescue Exception => e
Bởi vì Exception là Class cha của mọi exception khi thực thi đoạn code này hệ thống cần phải lookup tất cả các ngoại lệ trong toàn bộ hệ thống phân cấp exceptions và tìm ra chính xác exception mà nó gặp phải. điều này hạn chế tốc độ xử lý rất nhiều.
- Nên rescure ra chính xác ngoại lệ thì sẽ tốt hơn, Trong trường hợp không biết chính xác ngoại lệ thì nên dùng Class StandardError.
Class StandardError chứa khá đầy đủ các ngoại lệ:
StandardError FiberError ThreadError IndexError StopIteration KeyError Math::DomainError LocalJumpError IOError EOFError EncodingError Encoding::ConverterNotFoundError Encoding::InvalidByteSequenceError Encoding::UndefinedConversionError Encoding::CompatibilityError RegexpError SystemCallError Errno::ERPCMISMATCH Errno::NOERROR RangeError FloatDomainError ZeroDivisionError RuntimeError Gem::Exception NameError NoMethodError ArgumentError Gem::Requirement::BadRequirementError TypeError
Thông thường mình thường viết
rescue => e
đây cũng là cách viết tắt của:
rescue StandardError => e
4. Kết luận
Xử lý ngoại lệ là điều rất cần thiết trong mọi dự án. Đừng để chương trình của bạn dừng lại một cách đột ngột mà không hề có bất kì hành động giải thích gì đến người dùng.
Trong khi xử lý ngoại lệ với Ruby bạn nên hạn chế đưa ra ngoại lệ của Class Exception thay vào đó là chỉ ra một Ngoại lệ cụ thể hơn.
Cảm ơn!
Tham khảo
http://rubylearning.com/satishtalim/ruby_exceptions.html
http://phrogz.net/programmingruby/tut_exceptions.html
http://daniel.fone.net.nz/blog/2013/05/28/why-you-should-never-rescue-exception-in-ruby/