Kelas kesalahan khusus Ruby: pewarisan atribut pesan

95

Sepertinya saya tidak dapat menemukan banyak informasi tentang kelas pengecualian khusus.

Apa yang saya tahu

Anda bisa mendeklarasikan kelas kesalahan kustom Anda dan membiarkannya StandardErrorditurunkan, jadi bisa jadi rescued:

class MyCustomError < StandardError
end

Ini memungkinkan Anda untuk meningkatkannya menggunakan:

raise MyCustomError, "A message"

dan nanti, dapatkan pesan itu saat menyelamatkan

rescue MyCustomError => e
  puts e.message # => "A message"

Apa yang saya tidak tahu

Saya ingin memberikan pengecualian saya beberapa bidang khusus, tetapi saya ingin mewarisi messageatribut dari kelas induk. Saya menemukan membaca tentang topik ini yang @messagebukan merupakan variabel instance dari kelas pengecualian, jadi saya khawatir warisan saya tidak akan berfungsi.

Adakah yang bisa memberi saya detail lebih lanjut tentang ini? Bagaimana cara menerapkan kelas kesalahan khusus dengan objectatribut? Apakah yang berikut ini benar:

class MyCustomError < StandardError
  attr_reader :object
  def initialize(message, object)
    super(message)
    @object = object
  end
end

Lalu:

raise MyCustomError.new(anObject), "A message"

mendapatkan:

rescue MyCustomError => e
  puts e.message # => "A message"
  puts e.object # => anObject

apakah itu akan berhasil, dan jika berhasil, apakah ini cara yang benar dalam melakukan sesuatu?

MarioDS
sumber
3
Jangan rescue Exception => e. Ini lebih luas dari default rescue => eyang diturunkan dari StandardError, dan menangkap semuanya termasuk Ctrl + C. Aku akan melakukannya rescue MyCustomError => e.
Ryan Taylor
1
@RyanTaylor Saya mengedit pertanyaan saya untuk pendekatan yang lebih tepat.
MarioDS

Jawaban:

121

raise sudah menyetel pesan sehingga Anda tidak perlu meneruskannya ke konstruktor:

class MyCustomError < StandardError
  attr_reader :object

  def initialize(object)
    @object = object
  end
end

begin
  raise MyCustomError.new("an object"), "a message"
rescue MyCustomError => e
  puts e.message # => "a message"
  puts e.object # => "an object"
end

Saya telah menggantinya rescue Exceptiondengan rescue MyCustomError, lihat Mengapa gaya `rescue Exception => e` di Ruby merupakan gaya yang buruk? .

Stefan
sumber
Saya akan menerima jawaban Anda karena Anda menunjukkan kepada saya seluruh sintaks. Terima kasih!
MarioDS
1
Di sini kita lakukan rescue Exception, tapi kenapa tidak rescue MyCustomError?
Dfr
FYI, jika argumen pertama, objek, adalah opsi dan raise MyCustomError, "a message"tanpa new, "pesan" tidak akan disetel.
hiroshi
Apakah ada cara untuk mendapatkan pesan yang muncul di kelas pengecualian kustom kami?
CyberMew
@Cyber ​​apa maksudmu? Apa yang ingin kamu lakukan?
Stefan
10

Mengingat apa yang Exceptiondinyatakan oleh dokumentasi inti rubi , yang mewarisi semua kesalahan lainnya, nyatakan#message

Menampilkan hasil pemanggilan exception.to_s. Biasanya ini mengembalikan pesan atau nama pengecualian. Dengan menyediakan metode to_str, pengecualian setuju untuk digunakan di tempat yang diharapkan Strings.

http://ruby-doc.org/core-1.9.3/Exception.html#method-i-message

Saya akan memilih untuk mendefinisikan ulang to_s/ to_stratau penginisialisasi. Berikut adalah contoh di mana kami ingin tahu, dengan cara yang sebagian besar dapat dibaca manusia, ketika layanan eksternal gagal melakukan sesuatu.

CATATAN: Strategi kedua di bawah ini menggunakan metode string rails yang cantik, seperti demodualize, yang mungkin sedikit rumit dan karena itu berpotensi tidak bijaksana untuk dilakukan dalam pengecualian. Anda juga bisa menambahkan lebih banyak argumen ke tanda tangan metode, jika Anda membutuhkannya.

Mengganti Strategi #to_s bukan #to_str, cara kerjanya berbeda

module ExternalService

  class FailedCRUDError < ::StandardError
    def to_s
      'failed to crud with external service'
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

Output Konsol

begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "failed to crud with external service"

begin; raise ExternalService::FailedToCreateError, 'custom message'; rescue => e; e.message; end
# => "failed to crud with external service"

begin; raise ExternalService::FailedToCreateError.new('custom message'); rescue => e; e.message; end
# => "failed to crud with external service"

raise ExternalService::FailedToCreateError
# ExternalService::FailedToCreateError: failed to crud with external service

Mengganti Strategi #initialize

Ini adalah strategi yang paling dekat dengan implementasi yang saya gunakan di rel. Seperti disebutkan di atas, menggunakan demodualize, underscoredan humanize ActiveSupportmetode. Tapi ini bisa dengan mudah dihilangkan, seperti pada strategi sebelumnya.

module ExternalService
  class FailedCRUDError < ::StandardError
    def initialize(service_model=nil)
      super("#{self.class.name.demodulize.underscore.humanize} using #{service_model.class}")
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

Output Konsol

begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "Failed to create error using NilClass"

begin; raise ExternalService::FailedToCreateError, Object.new; rescue => e; e.message; end
# => "Failed to create error using Object"

begin; raise ExternalService::FailedToCreateError.new(Object.new); rescue => e; e.message; end
# => "Failed to create error using Object"

raise ExternalService::FailedCRUDError
# ExternalService::FailedCRUDError: Failed crud error using NilClass

raise ExternalService::FailedCRUDError.new(Object.new)
# RuntimeError: ExternalService::FailedCRUDError using Object

Alat Demo

Ini adalah demo untuk menunjukkan penyelamatan dan pengiriman pesan dari implementasi di atas. Kelas yang memunculkan pengecualian adalah API palsu ke Cloudinary. Cukup buang salah satu strategi di atas ke konsol rel Anda, diikuti dengan ini.

require 'rails' # only needed for second strategy 

module ExternalService
  class FailedCRUDError < ::StandardError
    def initialize(service_model=nil)
      @service_model = service_model
      super("#{self.class.name.demodulize.underscore.humanize} using #{@service_model.class}")
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

# Stub service representing 3rd party cloud storage
class Cloudinary

  def initialize(*error_args)
    @error_args = error_args.flatten
  end

  def create_read_update_or_delete
    begin
      try_and_fail
    rescue ExternalService::FailedCRUDError => e
      e.message
    end
  end

  private def try_and_fail
    raise *@error_args
  end
end

errors_map = [
  # Without an arg
  ExternalService::FailedCRUDError,
  ExternalService::FailedToCreateError,
  ExternalService::FailedToReadError,
  ExternalService::FailedToUpdateError,
  ExternalService::FailedToDeleteError,
  # Instantiated without an arg
  ExternalService::FailedCRUDError.new,
  ExternalService::FailedToCreateError.new,
  ExternalService::FailedToReadError.new,
  ExternalService::FailedToUpdateError.new,
  ExternalService::FailedToDeleteError.new,
  # With an arg
  [ExternalService::FailedCRUDError, Object.new],
  [ExternalService::FailedToCreateError, Object.new],
  [ExternalService::FailedToReadError, Object.new],
  [ExternalService::FailedToUpdateError, Object.new],
  [ExternalService::FailedToDeleteError, Object.new],
  # Instantiated with an arg
  ExternalService::FailedCRUDError.new(Object.new),
  ExternalService::FailedToCreateError.new(Object.new),
  ExternalService::FailedToReadError.new(Object.new),
  ExternalService::FailedToUpdateError.new(Object.new),
  ExternalService::FailedToDeleteError.new(Object.new),
].inject({}) do |errors, args|
  begin 
    errors.merge!( args => Cloudinary.new(args).create_read_update_or_delete)
  rescue => e
    binding.pry
  end
end

if defined?(pp) || require('pp')
  pp errors_map
else
  errors_map.each{ |set| puts set.inspect }
end
Chad M
sumber
6

Ide Anda benar, tetapi cara Anda menyebutnya salah. Harus

raise MyCustomError.new(an_object, "A message")
sawa
sumber
Oke, saya pikir pesan yang Anda berikan adalah parameter kedua untuk raisekata kunci atau sesuatu.
MarioDS
Anda mendefinisikan ulang initializeuntuk mengambil dua argumen. newmeneruskan argumen ke initialize.
sawa
Atau, Anda dapat menghilangkan tanda kurung.
sawa
Saya mengerti sedikit itu, tapi poster dari topik yang saya terhubung ke dalam pertanyaan saya melakukannya seperti ini: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Jadi dia memanggil raisedengan dua parameter: BillRowErrorobjek baru , dan pesannya. Saya hanya bingung dengan sintaksnya ... Di tutorial lain saya selalu melihatnya seperti ini:raise Error, message
MarioDS
1
Masalahnya bukan pada berapa banyak argumen yang Anda berikan raise; itu cukup fleksibel. Masalahnya adalah Anda mendefinisikan initializeuntuk mengambil dua argumen dan hanya memberi satu. Lihat contoh Anda. BillRowError.new(:roamingcalls, @index)diberikan dua argumen.
sawa
5

Saya ingin melakukan hal serupa. Saya ingin meneruskan objek ke #new dan mengatur pesan berdasarkan beberapa pemrosesan objek yang diteruskan. Karya berikut ini.

class FooError < StandardError
  attr_accessor :message # this is critical!
  def initialize(stuff)
    @message = stuff.reverse
  end
end

begin
  raise FooError.new("!dlroW olleH")
rescue FooError => e
  puts e.message #=> Hello World!
end

Perhatikan bahwa jika Anda tidak mendeklarasikannya attr_accessor :messagemaka itu tidak akan berhasil. Mengatasi masalah OP, Anda juga bisa meneruskan pesan sebagai argumen tambahan dan menyimpan apapun yang Anda suka. Bagian krusial tampaknya menggantikan #message.

Huliax
sumber