Ruby.Metaprogramming. class_eval

前端 未结 5 661
孤独总比滥情好
孤独总比滥情好 2020-12-18 16:50

There seem to be a mistake in my code. However I just can\'t find it out.

class Class
def attr_accessor_with_history(attr_name)
  attr_name = attr_name.to_s
         


        
相关标签:
5条回答
  • 2020-12-18 17:27

    Solution:

    class Class
      def attr_accessor_with_history(attr_name)
        ivar         = "@#{attr_name}"
        history_meth = "#{attr_name}_history"
        history_ivar = "@#{history_meth}"
    
        define_method(attr_name) { instance_variable_get ivar }
    
        define_method "#{attr_name}=" do |value|
          instance_variable_set ivar, value
          instance_variable_set history_ivar, send(history_meth) << value
        end
    
        define_method history_meth do
          value = instance_variable_get(history_ivar) || []
          value.dup
        end
      end
    end
    

    Tests:

    describe 'Class#attr_accessor_with_history' do
      let(:klass)     { Class.new { attr_accessor_with_history :bar } }
      let(:instance)  { instance = klass.new }
    
      it 'acs as attr_accessor' do
        instance.bar.should be_nil
        instance.bar = 1
        instance.bar.should == 1
        instance.bar = 2
        instance.bar.should == 2
      end
    
      it 'remembers history of setting' do
        instance.bar_history.should == []
        instance.bar = 1
        instance.bar_history.should == [1]
        instance.bar = 2
        instance.bar_history.should == [1, 2]
      end
    
      it 'is not affected by mutating the history array' do
        instance.bar_history << 1
        instance.bar_history.should == []
        instance.bar = 1
        instance.bar_history << 2
        instance.bar_history.should == [1]
      end
    end
    
    0 讨论(0)
  • 2020-12-18 17:29

    You will find a solution for your problem in Sergios answer. Here an explanation, what's going wrong in your code.

    With

    class_eval %Q{
     @#{attr_name}_history=[1,2,3]
    }
    

    you execute

     @bar_history = [1,2,3]
    

    You execute this on class level, not in object level. The variable @bar_history is not available in a Foo-object, but in the Foo-class.

    With

    puts f.bar_history.to_s
    

    you access the -never on object level defined- attribute @bar_history.

    When you define a reader on class level, you have access to your variable:

    class << Foo 
      attr_reader :bar_history
    end
    p Foo.bar_history  #-> [1, 2, 3]
    
    0 讨论(0)
  • 2020-12-18 17:30

    @Sergio Tulentsev's answer works, but it promotes a problematic practice of using string eval which is in general fraught with security risks and other surprises when the inputs aren't what you expect. For example, what happens to Sergio's version if one calls (no don't try it):

    attr_accessor_with_history %q{foo; end; system "rm -rf /"; def foo}
    

    It is often possible to do ruby meta-programming more carefully without string eval. In this case, using simple interpolation and define_method of closures with instance_variable_[get|set], and send:

    module History
    
      def attr_accessor_with_history(attr_name)
        getter_sym  = :"#{attr_name}"
        setter_sym  = :"#{attr_name}="
        history_sym = :"#{attr_name}_history"
        iv_sym      = :"@#{attr_name}"
        iv_hist     = :"@#{attr_name}_history"
    
        define_method getter_sym do
          instance_variable_get(iv_sym)
        end
    
        define_method setter_sym do |val|
          instance_variable_set( iv_hist, [] ) unless send(history_sym)
          send(history_sym).send( :'<<', send(getter_sym) )
          instance_variable_set( iv_sym, val @)
        end
    
        define_method history_sym do
          instance_variable_get(iv_hist)
        end
    
      end
    end
    
    0 讨论(0)
  • 2020-12-18 17:33

    You shouldn't be opening Class to add new methods. That's what modules are for.

    module History
      def attr_accessor_with_history(attr_name)
        attr_name = attr_name.to_s
    
        attr_accessor attr_name
    
        class_eval %Q{
          def #{attr_name}_history
            [1, 2, 3]
          end
        }
    
      end
    end
    
    class Foo
      extend History
      attr_accessor_with_history :bar
    end
    
    f = Foo.new
    f.bar = 1
    f.bar = 2
    puts f.bar_history.inspect
    # [1, 2, 3]
    

    And here's the code you probably meant to write (judging from the names).

    module History
      def attr_accessor_with_history(attr_name)
        attr_name = attr_name.to_s
    
        class_eval %Q{
          def #{attr_name}
            @#{attr_name}
          end
    
          def #{attr_name}= val
            @#{attr_name}_history ||= []
            @#{attr_name}_history << #{attr_name}
    
            @#{attr_name} = val
          end
    
          def #{attr_name}_history
            @#{attr_name}_history
          end
        }
    
      end
    end
    
    class Foo
      extend History
      attr_accessor_with_history :bar
    end
    
    f = Foo.new
    f.bar = 1
    f.bar = 2
    puts f.bar_history.inspect
    # [nil, 1]
    
    0 讨论(0)
  • 2020-12-18 17:37

    Here is what should be done. The attr_writer need be defined withing class_eval instead in Class.

    class Class
      def attr_accessor_with_history(attr_name)
        attr_name = attr_name.to_s
    
        attr_reader attr_name
        #attr_writer attr_name  ## moved into class_eval
    
        attr_reader attr_name + "_history"
    
        class_eval %Q{
          def #{attr_name}=(value)
            @#{attr_name}_history=[1,2,3]
          end
        }
    
    end
    end
    
    0 讨论(0)
提交回复
热议问题