Xử lý Ngoại lệ trong Ruby
Bài đăng này đã không được cập nhật trong 8 năm
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, "\n"
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/
All rights reserved