Niestandardowe klasy błędów Rubiego: dziedziczenie atrybutu komunikatu


95

Nie mogę znaleźć wielu informacji o niestandardowych klasach wyjątków.

Co ja wiem

Możesz zadeklarować swoją niestandardową klasę błędu i pozwolić jej dziedziczyć z StandardError, więc może być rescued:

class MyCustomError < StandardError
end

Pozwala to podnieść go za pomocą:

raise MyCustomError, "A message"

a później otrzymaj tę wiadomość podczas ratowania

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

Czego nie wiem

Chcę dać mojemu wyjątkowi niektóre pola niestandardowe, ale chcę odziedziczyć messageatrybut z klasy nadrzędnej. Dowiedziałem się, czytając na ten temat, że @messagenie jest zmienną instancji klasy wyjątku, więc martwię się, że moje dziedziczenie nie zadziała.

Czy ktoś może podać mi więcej szczegółów na ten temat? Jak zaimplementować niestandardową klasę błędu z objectatrybutem? Czy poniższe informacje są poprawne:

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

I wtedy:

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

dostać:

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

czy to zadziała, a jeśli tak, to czy jest to właściwy sposób postępowania?


3
Nie rescue Exception => e. Jest szerszy niż domyślny, rescue => ektóry rozciąga się od StandardErrori przechwytuje wszystko, w tym Ctrl + C. Zrobiłbym rescue MyCustomError => e.
Ryan Taylor

1
@RyanTaylor Zmieniłem pytanie, aby uzyskać bardziej odpowiednie podejście.
MarioDS

Odpowiedzi:


121

raise ustawia już wiadomość, więc nie musisz jej przekazywać do konstruktora:

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

Mam wymienić rescue Exceptionz rescue MyCustomErrorzobacz Dlaczego jest to zły styl `Exception ratunkowej => e` w Ruby? .


Przyjmę twoją odpowiedź, ponieważ pokazałeś mi całą składnię. Dzięki!
MarioDS

1
Tutaj robimy rescue Exception, ale dlaczego nie rescue MyCustomError?
Dfr

FYI, jeśli pierwszy argument, obiekt, jest opcją, a raise MyCustomError, "a message"bez niej new, „komunikat” nie zostanie ustawiony.
Hiroshi

Czy istnieje sposób, aby jakoś uzyskać podniesiony komunikat w naszej niestandardowej klasie wyjątków?
CyberMew

@CyberMew co masz na myśli? Co chcesz robić?
Stefan

10

Biorąc pod uwagę, o czym mówi dokumentacja Ruby Core Exception, z której dziedziczą wszystkie inne błędy#message

Zwraca wynik wywołania wyjątku.to_s. Zwykle zwraca to wiadomość lub nazwę wyjątku. Dostarczając metodę to_str, wyjątki zgadzają się na użycie tam, gdzie oczekuje się ciągów.

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

Wybrałbym redefinicję to_s/ to_strlub inicjalizator. Oto przykład, w którym chcemy wiedzieć, w sposób w większości czytelny dla człowieka, kiedy usługa zewnętrzna czegoś nie zrobiła.

UWAGA: Druga strategia poniżej wykorzystuje ładne metody łańcuchowe rails, takie jak demodualize, które mogą być nieco skomplikowane, a zatem potencjalnie nierozsądne w przypadku wyjątków. W razie potrzeby możesz również dodać więcej argumentów do podpisu metody.

Zastępowanie strategii #to_s, a nie #to_str, działa inaczej

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

Wyjście konsoli

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

Zastępowanie #initialize Strategy

To strategia najbliższa implementacjom, których używałem w railsach. Jak wspomniano powyżej, używa się demodualize, underscorei humanize ActiveSupportmetod. Ale można to łatwo usunąć, tak jak w poprzedniej strategii.

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

Wyjście konsoli

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

Narzędzie demonstracyjne

To jest demo pokazujące ratowanie i wysyłanie wiadomości z powyższej implementacji. Klasa podnosząca wyjątki to fałszywe API do Cloudinary. Po prostu wrzuć jedną z powyższych strategii do konsoli rails, a następnie wykonaj tę.

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

6

Twój pomysł jest słuszny, ale sposób, w jaki go nazywasz, jest zły. Powinno być

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

Okay, myślałem, że przekazana przez Ciebie wiadomość była drugim parametrem raisesłowa kluczowego czy coś w tym stylu.
MarioDS

Przedefiniowałeś, initializeaby przyjąć dwa argumenty. newprzekazuje argumenty do initialize.
sawa

Możesz też pominąć nawiasy.
sawa

Rozumiem, że trochę, ale plakat tematu I połączone w moim pytaniu robi to tak: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Dlatego wywołuje raisez dwoma parametrami: nowy BillRowErrorobiekt i jego wiadomość. Jestem po prostu zdezorientowany składnią ... W innych tutorialach zawsze widzę to tak:raise Error, message
MarioDS

1
Problem nie polega na tym, do ilu argumentów przekazujesz raise; to jest dość elastyczne. Problem polega na tym, że zdefiniowałeś initializedwa argumenty i podałeś tylko jeden. Spójrz na swój przykład. BillRowError.new(:roamingcalls, @index)otrzymuje dwa argumenty.
sawa

5

Chciałem zrobić coś podobnego. Chciałem przekazać obiekt do #new i ustawić komunikat w oparciu o przetwarzanie przekazanego obiektu. Następujące prace.

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

Zauważ, że jeśli nie zadeklarujesz, attr_accessor :messageto nie zadziała. Rozwiązując problem OP, możesz również przekazać wiadomość jako dodatkowy argument i zapisać wszystko, co chcesz. Wydaje się, że kluczową częścią jest nadpisanie #message.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.