+5

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ý đượ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:

[email protected]:~/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 ensureelse

Đô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

Viblo
Let's register a Viblo Account to get more interesting posts.