Пользовательские классы ошибок Ruby: наследование атрибута сообщения

95

Я не могу найти много информации о пользовательских классах исключений.

Что я знаю

Вы можете объявить свой собственный класс ошибок и позволить ему наследовать от него StandardError, поэтому он может быть rescued:

class MyCustomError < StandardError
end

Это позволяет поднять его, используя:

raise MyCustomError, "A message"

а позже получите это сообщение при спасении

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

Что я не знаю

Я хочу предоставить своему исключению несколько настраиваемых полей, но я хочу унаследовать messageатрибут от родительского класса. Я прочитал по этой теме, что @messageне является переменной экземпляра класса исключения, поэтому я беспокоюсь, что мое наследование не сработает.

Кто-нибудь может дать мне более подробную информацию об этом? Как мне реализовать собственный класс ошибок с objectатрибутом? Правильно ли следующее:

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

А потом:

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

получить:

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

будет ли это работать, и если да, то правильно ли это?

MarioDS
источник
3
Не надо rescue Exception => e. Он шире, чем значение по умолчанию, rescue => eкоторое продолжается от StandardErrorи улавливает все, включая Ctrl + C. Я бы сделал rescue MyCustomError => e.
Райан Тейлор,
1
@RyanTaylor Я отредактировал свой вопрос для более правильного подхода.
MarioDS

Ответы:

121

raise уже устанавливает сообщение, поэтому вам не нужно передавать его конструктору:

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

Я заменил его rescue Exceptionна rescue MyCustomError, см. Почему это плохой стиль для `rescue Exception => e` в Ruby? .

Стефан
источник
Я принимаю ваш ответ, потому что вы показали мне весь синтаксис. Спасибо!
MarioDS
1
Вот мы и делаем rescue Exception, а почему бы и нет rescue MyCustomError?
Dfr
К вашему сведению, если первый аргумент, объект, является опцией и raise MyCustomError, "a message"без него new, «сообщение» не будет установлено.
hiroshi
Есть ли способ каким-то образом получить поднятое сообщение в нашем пользовательском классе исключений?
CyberMew
@CyberMew, что ты имеешь в виду? Что ты хочешь делать?
Стефан
10

Учитывая, что в основной документации Ruby Exception, от которой наследуются все остальные ошибки, говорится о#message

Возвращает результат вызова exception.to_s. Обычно это возвращает сообщение или имя исключения. Предоставляя метод to_str, исключения соглашаются использоваться там, где ожидаются строки.

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

Я бы выбрал новое определение to_s / to_strили инициализатор. Вот пример, в котором мы хотим знать, в основном доступным для понимания человеком способом, когда внешняя служба что-то не смогла сделать.

ПРИМЕЧАНИЕ. Вторая стратегия ниже использует довольно строковые методы rails, такие как demodualize , которые могут быть немного сложными и, следовательно, потенциально неразумными в исключении. Вы также можете добавить дополнительные аргументы в подпись метода, если вам это нужно.

Переопределение стратегии #to_s, а не #to_str, работает иначе

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

Консольный вывод

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

Переопределение стратегии #initialize

Это стратегия, наиболее близкая к реализациям, которые я использовал в рельсах. Как было отмечено выше, он использует demodualize,underscore и humanize ActiveSupportметоду. Но это можно было легко удалить, как и в предыдущей стратегии.

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

Консольный вывод

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

Демо-инструмент

Это демонстрация, демонстрирующая спасение и обмен сообщениями вышеупомянутой реализации. Класс, вызывающий исключения, - это поддельный API Cloudinary. Просто загрузите одну из вышеперечисленных стратегий в свою консоль rails, а затем выполните эту.

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

Ваша идея верна, но то, как вы ее называете, неверно. Так должно быть

raise MyCustomError.new(an_object, "A message")
сава
источник
Хорошо, я думал, что сообщение, которое вы дали, было вторым параметром raiseключевого слова или чего-то в этом роде.
MarioDS
Вы переопределили initializeдва аргумента. newпередает аргументы в initialize.
sawa
Или вы можете опустить круглые скобки.
sawa
Я понимаю , что немного, но плакат на тему я связан в моем вопросе делает это так: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Поэтому он вызывает raiseс двумя параметрами: новый BillRowErrorобъект и его сообщение. Меня просто сбивает с толку синтаксис ... В других уроках я всегда вижу это так:raise Error, message
MarioDS
1
Проблема не в том, сколько аргументов вы передаете raise; это довольно гибко. Проблема в том, что вы решили initializeпринимать два аргумента и дали только один. Посмотрите на свой пример. BillRowError.new(:roamingcalls, @index)приводится два аргумента.
sawa
5

Я хотел сделать нечто подобное. Я хотел передать объект #new и установить сообщение на основе некоторой обработки переданного объекта. Следующие работы.

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

Обратите внимание: если вы не объявите, attr_accessor :messageэто не сработает. Обращаясь к проблеме OP, вы также можете передать сообщение в качестве дополнительного аргумента и сохранить все, что захотите. Важнейшей частью, похоже, является #message.

Huliax
источник