Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ruby Exceptions: คู่มือแบบเป็นขั้นตอนสำหรับมือใหม่

วันหนึ่งผมค้นหาบทนำ Ruby exceptions สำหรับมือใหม่ — คนรู้ basic Ruby syntax แต่ไม่แน่ใจว่า exception คืออะไร ทำไมถึงมีประโยชน์ ไม่เจอ จึงลองเขียนเอง หวังว่าจะมีประโยชน์ หากจุดไหนสับสน tweet มาที่ @StarrHorne :)

Exception คืออะไร?

Exceptions เป็นวิธีของ Ruby จัดการ unexpected events

เคย typo ทำโปรแกรม crash พร้อมข้อความ SyntaxError หรือ NoMethodError? นั่นคือ exceptions ทำงาน

เมื่อ raise exception ใน Ruby โลกหยุดนิ่ง โปรแกรมเริ่ม shutdown หากไม่มีอะไรหยุด ก็จะ exit พร้อม error message

ตัวอย่าง: ลองหารด้วยศูนย์ ไม่ได้ Ruby raise exception ZeroDivisionError โปรแกรมหยุดพิมพ์ error

1 / 0
# Program crashes และ outputs: "ZeroDivisionError: divided by 0"

Crashing programs ทำ users โกรธ ปกติอยากหยุด shutdown process และตอบสนอง error อย่างฉลาด

เรียกว่า "rescuing", "handling", หรือ "catching" exception หมายความเหมือนกัน ทำใน Ruby แบบนี้:

begin
  # Any exceptions in here...
  1/0
rescue
  # ...will cause this code to run
  puts "Got an exception, but I'm responding intelligently!"
  do_something_intelligent()
end
# โปรแกรมนี้ไม่ crash
# Outputs: "Got an exception, but I'm responding intelligently!"

Exception ยังเกิด แต่ไม่ crash เพราะ "rescued" แทนที่จะ exit Ruby รันโค้ดใน rescue block พิมพ์ข้อความ

ดีแต่มีข้อจำกัดใหญ่: บอกว่า "something went wrong" โดยไม่บอกว่า อะไร ผิด

ข้อมูลทั้งหมดเกี่ยวกับสิ่งที่ผิดอยู่ใน exception object

Exception Objects

Exception objects เป็น Ruby objects ธรรมดา เก็บข้อมูล "what happened" สำหรับ exception ที่ rescue

เพื่อรับ exception object ใช้ rescue syntax แบบต่างออกไป:

# Rescue all errors, ใส่ exception object ใน `e`
rescue => e
# Rescue เฉพาะ ZeroDivisionError ใส่ exception object ใน `e`
rescue ZeroDivisionError => e

ตัวอย่างที่สอง ZeroDivisionError เป็น class ของ object ใน e "types" ของ exceptions ที่พูดถึงคือ class names

Exception objects เก็บ debug data ที่มีประโยชน์ ดู exception object สำหรับ ZeroDivisionError

begin
  1/0
rescue ZeroDivisionError => e
  puts "Exception Class: #{ e.class.name }"
  puts "Exception Message: #{ e.message }"
  puts "Exception Backtrace: #{ e.backtrace }"
end
# Outputs:
# Exception Class: ZeroDivisionError
# Exception Message: divided by 0
# Exception Backtrace: ...backtrace as an array...

เหมือน Ruby exceptions หลายตัว มี message, backtrace และ class name

Raise Exceptions เอง

จนถึงตอนนี้ rescue exceptions แต่มัน trigger เองได้ เรียกว่า "raising" ทำโดยเรียกเมธอด raise

ตอน raise เอง เลือก exception type ได้ ตั้ง error message ได้

ตัวอย่าง:

begin
  # raises ArgumentError with message "you messed up!"
  raise ArgumentError.new("You messed up!")
rescue ArgumentError => e
  puts e.message
end
# Outputs: You messed up!

สร้าง error object ใหม่ (ArgumentError) พร้อม custom message ส่งให้ raise

เป็น Ruby raise เรียกได้หลายแบบ:

# โปรดเพราะ explicit
raise RuntimeError.new("You messed up!")
# ...ผลลัพธ์เหมือนกัน
raise RuntimeError, "You messed up!"
# ...ผลลัพธ์เหมือนกัน แต่ raise RuntimeErrors ได้อย่างเดียว
raise "You messed up!"

สร้าง Custom Exceptions

Built-in exceptions ดี แต่ไม่ครอบคลุกทุก use case

ถ้าสร้าง user system อยาก raise exception ตอน user เข้าถึงส่วนห้าม ไม่มี standard exceptions ใดเข้ากัน สร้าง exception ใหม่ดีกว่า

สร้าง custom exception: สร้าง class ใหม่สืบทอดจาก StandardError

class PermissionDeniedError < StandardError
end

raise PermissionDeniedError.new()

แค่ Ruby class ปกติ เพิ่ม methods และ data ได้เหมือน class อื่น เพิ่ม attribute "action":

class PermissionDeniedError < StandardError
  attr_reader :action
  def initialize(message, action)
    super(message)
    @action = action
  end
end

# ตอน user พยายามลบสิ่งที่ไม่มี permission
raise PermissionDeniedError.new("Permission Denied", :delete)

Class Hierarchy

เราสร้าง custom exception โดย subclass StandardError ซึ่งสืบทอดจาก Exception

จริงๆ class hierarchy ของ exception ใดๆ ใน Ruby สุดท้ายไปถึง Exception พิสูจน์ให้ดู: built-in exceptions ส่วนใหญ่ แสดงแบบ hierarchy

Exception
  NoMemoryError
  ScriptError
    LoadError
    NotImplementedError
    SyntaxError
  SignalException
    Interrupt
  StandardError
    ArgumentError
    IOError
      EOFError
    IndexError
    LocalJumpError
    NameError
      NoMethodError
    RangeError
      FloatDomainError
      RegexpError
    RuntimeError
    SecurityError
    SystemCallError
    SystemStackError
    ThreadError
    TypeError
    ZeroDivisionError
  SystemExit

ไม่ต้องจดทั้งหมด แสดงเพราะ hierarchy concept นี้สำคัญมากเพราะเหตุผลเฉพาะ

Rescue errors ของ class เฉพาะ จะ rescue errors ของ child classes ด้วย

ลองอีกครั้ง...

เวลา rescue StandardError ไม่แค่ rescue exceptions class StandardError แต่ children ด้วย ดู chart จะเยอะ: ArgumentError, IOError ฯลฯ

ถ้า rescue Exception จะ rescue ทุก exception ซึ่งเป็นไอเดียแย่มาก

Rescue ทุก Exceptions (วิธีผิด)

อยากโดนด่า ไป stack overflow โพสโค้ดแบบนี้:

// Don't do this
begin
  do_something()
rescue Exception => e
  ...
end

โค้ดข้างต้น rescue ทุก exception อย่าทำ! จะทำให้โปรแกรมเสียแบบแปลก

เพราะ Ruby ใช้ exceptions ไม่ใช่แค่ errors ใช้จัดการ messages จาก OS ที่เรียก "Signals" เคยกด "ctrl-c" ออกโปรแกรม? ใช้ signal ด้วยการ suppress ทุก exceptions คุณกด suppress signals นั้นด้วย

ยังมี exceptions บางประเภท — เช่น syntax errors — ที่ควร crash โปรแกรม หาก suppress จะไม่รู้ตอน typo หรือผิดพลาดอื่นๆ

Rescue ทุก Errors (วิธีถูกต้อง)

กลับดู class hierarchy chart จะเห็น errors ทั้งหมดที่อยาก rescue เป็น children ของ StandardError

หมายถึงอยาก rescue "all errors" ควร rescue StandardError

begin
  do_something()
rescue StandardError => e
  # แค่ exceptions ของแอปคุณถูกกลืน SyntaxErrror เหลืออยู่
end

จริงๆ หากไม่ระบุ exception class Ruby สมมติว่าเป็น StandardError

begin
  do_something()
rescue => e
  # นี้เหมือน rescue StandardError
end

Rescue Specific Errors (วิธีที่ดีที่สุด)

ตอนรู้ rescue all errors ควรรู้ว่ามันมักเป็น bad idea, code smell, considered harmful ฯลฯ

มักบ่งชี้ว่า lazy ไม่ไปหาว่า exception ใดต้อง rescue และจะกลับมากวนตลอด

ใช้เวลาทำให้ถูกต้อง Rescue specific exceptions

begin
  do_something()
rescue Errno::ETIMEDOUT => e
  // Rescue เฉพาะ Errno::ETIMEDOUT exceptions
end

Rescue หลายชนิดใน rescue block เดียวได้ ไม่มีข้อคิดเห็น :)

begin
  do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end