Classi di errore personalizzate Ruby: eredità dell'attributo del messaggio


95

Non riesco a trovare molte informazioni sulle classi di eccezioni personalizzate.

Quello che so

Puoi dichiarare la tua classe di errore personalizzata e lasciarla ereditare da StandardError, quindi può essere rescued:

class MyCustomError < StandardError
end

Questo ti permette di aumentarlo usando:

raise MyCustomError, "A message"

e più tardi, ricevi quel messaggio durante il salvataggio

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

Quello che non so

Voglio dare alla mia eccezione alcuni campi personalizzati, ma voglio ereditare l' messageattributo dalla classe genitore. Ho scoperto di leggere su questo argomento che @messagenon è una variabile di istanza della classe di eccezione, quindi sono preoccupato che la mia eredità non funzioni.

Qualcuno può darmi maggiori dettagli su questo? Come implementerei una classe di errore personalizzata con un objectattributo? È corretto quanto segue:

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

E poi:

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

ottenere:

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

funzionerà, e se funziona, è questo il modo corretto di fare le cose?


3
Non farlo rescue Exception => e. È più ampio del valore predefinito rescue => eche si estende da StandardErrore cattura tutto, incluso Ctrl + C. Lo farei rescue MyCustomError => e.
Ryan Taylor,

1
@ RyanTaylor Ho modificato la mia domanda per un approccio più corretto.
MarioDS

Risposte:


121

raise imposta già il messaggio in modo da non doverlo passare al costruttore:

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

Ho sostituito rescue Exceptioncon rescue MyCustomError, vedi Perché è uno stile sbagliato per `rescue Exception => e` in Ruby? .


Accetterò la tua risposta perché mi hai mostrato l'intera sintassi. Grazie!
MarioDS

1
Ci stiamo facendo rescue Exception, ma perché no rescue MyCustomError?
Dfr

Cordiali saluti, se il primo argomento, oggetto, è un'opzione e raise MyCustomError, "a message"senza new, "un messaggio" non verrà impostato.
Hiroshi

C'è un modo per ottenere in qualche modo il messaggio sollevato nella nostra classe di eccezione personalizzata?
CyberMew

@CyberMew cosa intendi? Che cosa vuoi fare?
Stefan

10

Dato ciò di cui parla la documentazione di ruby ​​core Exception, da cui ereditano tutti gli altri errori#message

Restituisce il risultato della chiamata di exception.to_s. Normalmente questo restituisce il messaggio o il nome dell'eccezione. Fornendo un metodo to_str, le eccezioni accettano di essere utilizzate dove sono previste stringhe.

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

Opterei per ridefinire to_s/ to_stro l'inizializzatore. Ecco un esempio in cui vogliamo sapere, in un modo per lo più leggibile dall'uomo, quando un servizio esterno non è riuscito a fare qualcosa.

NOTA: la seconda strategia di seguito utilizza i metodi rails piuttosto string, come demodualize, che possono essere un po 'complicati e quindi potenzialmente poco saggi da fare in un'eccezione. Potresti anche aggiungere più argomenti alla firma del metodo, se necessario.

Sostituendo #to_s Strategy non #to_str, funziona in modo diverso

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

Uscita console

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

Sostituzione della strategia #initialize

Questa è la strategia più vicina alle implementazioni che ho usato in rails. Come notato sopra, utilizza le demodualize, underscoree humanize ActiveSupportmetodi. Ma questo potrebbe essere facilmente rimosso, come nella strategia precedente.

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

Uscita console

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

Strumento demo

Questa è una demo per mostrare il salvataggio e la messaggistica dell'implementazione di cui sopra. La classe che solleva le eccezioni è una falsa API per Cloudinary. Basta scaricare una delle strategie di cui sopra nella tua console rails, seguita da questa.

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

La tua idea è giusta, ma il modo in cui la chiami è sbagliato. Dovrebbe essere

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

Ok, ho pensato che il messaggio che hai dato fosse un secondo parametro della raiseparola chiave o qualcosa del genere.
MarioDS

Hai ridefinito initializeper prendere due argomenti. newpassa gli argomenti a initialize.
sawa

Oppure puoi omettere le parentesi.
sawa

Capisco che po ', ma il manifesto del tema ho collegato nella mia interrogazione lo fa in questo modo: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Quindi chiama raisecon due parametri: un nuovo BillRowErroroggetto e il suo messaggio. Sono solo confuso dalla sintassi ... Su altri tutorial lo vedo sempre così:raise Error, message
MarioDS

1
Il problema non è con quanti argomenti passi raise; questo è abbastanza flessibile. Il problema è che hai definito initializedi prendere due argomenti e ne hai dato solo uno. Guarda nel tuo esempio. BillRowError.new(:roamingcalls, @index)vengono forniti due argomenti.
sawa

5

Volevo fare qualcosa di simile. Volevo passare un oggetto a #new e impostare il messaggio in base a un'elaborazione dell'oggetto passato. I seguenti lavori.

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

Nota che se non lo dichiari attr_accessor :message, non funzionerà. Affrontando il problema dell'OP, potresti anche passare il messaggio come argomento aggiuntivo e memorizzare tutto ciò che ti piace. La parte cruciale sembra essere l'override di #message.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.