Как использовать хеш в выражении class_eval в Ruby

Я работал над домашним заданием, когда столкнулся с неприятной проблемой. Назначение - это упражнение в метапрограммировании Ruby, и цель состоит в том, чтобы определить attr_accessor_with_history, который выполняет все те же действия, что и attr_accessor, но также предоставляет историю всех значений, которыми когда-либо был атрибут. Вот предоставленный код из назначения вместе с некоторым кодом, который я добавил в попытке выполнить назначение:

    class Class

  def attr_accessor_with_history(attr_name)
    attr_name = attr_name.to_s
    attr_hist_name = attr_name+'_history'
    history_hash = {attr_name => []}

    #getter
    self.class_eval("def #{attr_name} ; @#{attr_name} ; end")
    #setter
    self.class_eval %Q{
      def #{attr_name}=(val)
        # add to history
        @#{attr_hist_name} = [nil] if @#{attr_hist_name}.nil?
        @#{attr_hist_name} << val
        history_hash[@#{attr_name}] = @#{attr_hist_name}

        # set the value itself
        @#{attr_name} = val
      end

      def history(attr) ; @history_hash[attr.to_s] ; end
    }
  end
end

class Foo
  attr_accessor_with_history :bar
  attr_accessor_with_history :crud
end
f = Foo.new     # => #<Foo:0x127e678>
f.bar = 3       # => 3
f.bar = :wowzo  # => :wowzo
f.bar = 'boo!'  # => 'boo!'
puts f.history(:bar) # => [3, :wowzo, 'boo!']
f.crud = 42
f.crud = "Hello World!"
puts f.history(:crud)

Я хотел использовать хеш для хранения разных историй для разных атрибутов, но я не могу получить доступ к этому хешу в выражении class_eval для установщика. Независимо от того, как я пытаюсь его настроить, я всегда либо получаю NoMethodError для метода []=, потому что history_hash каким-то образом становится типом NilClass, либо происходит NameError, потому что он видит history_hash как неопределенную локальную переменную или метод. Как мне использовать хеш в выражениях class_eval?

1 ответ

Решение

или NameError происходит, потому что он видит "history_hash" как неопределенную локальную переменную или метод

Я бы сказал, что вы не можете, потому что это локальная переменная, которая недоступна в контексте, который вы хотите. Однако зачем вам это вообще нужно? Я вполне уверен, что это "некоторый код, который я добавил в попытке выполнить назначение", а не исходный код назначения (который, я полагаю, ожидает, что вы сохраните историю @bar в @bar_history - или что еще attr_hist_name все о?)

Я также чувствую себя неловко из-за обрывов строк; как правило, в этом нет необходимости, и Ruby может добиться большего успеха благодаря мощным средствам метапрограммирования. Вот как я это сделаю:

class Class
  def attr_accessor_with_history(attr_name)
    attr_setter_name = :"#{attr_name}="
    attr_getter_name = :"#{attr_name}"
    attr_hist_name = :"@#{attr_name}_history"
    attr_name = :"@#{attr_name}"

    self.class_eval do
      define_method(attr_getter_name) do
        instance_variable_get(attr_name)
      end

      define_method(attr_setter_name) do |val|
        instance_variable_set(attr_name, val)
        history = instance_variable_get(attr_hist_name)
        instance_variable_set(attr_hist_name, history = []) unless history
        history << val
      end
    end
  end
end

class Object
  def history(attr_name)
    attr_hist_name = :"@#{attr_name}_history"
    instance_variable_get(attr_hist_name)
  end
end

Наконец, так как это базовые классы, исправляющие обезьяны, я бы предпочел использовать уточнения, чтобы добавлять их там, где это необходимо, но это, вероятно, излишнее назначение.

Другие вопросы по тегам