Ruby.Metaprogramming。 class_eval

我的代码中似乎有一个错误。 但是我无法找到它。

class Class def attr_accessor_with_history(attr_name) attr_name = attr_name.to_s attr_reader attr_name attr_writer attr_name attr_reader attr_name + "_history" class_eval %Q{ @#{attr_name}_history=[1,2,3] } end end class Foo attr_accessor_with_history :bar end f = Foo.new f.bar = 1 f.bar = 2 puts f.bar_history.to_s 

我希望它返回一个数组[1,2,3] 。 但是,它不返回任何东西。

您将在Sergios答案中找到解决问题的方案。 这里有一个解释,你的代码出了什么问题。

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

你执行

  @bar_history = [1,2,3] 

您在类级别而不是在对象级别执行此操作。 变量@bar_history在Foo对象中不可用,但在Foo类中不可用。

 puts f.bar_history.to_s 

您在对象级别定义属性@bar_history上访问-never。

在类级别定义阅读器时,您可以访问您的变量:

 class << Foo attr_reader :bar_history end p Foo.bar_history #-> [1, 2, 3] 

您不应该打开Class来添加新方法。 这就是模块的用途。

 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] 

这里是您可能要编写的代码(从名称来看)。

 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] 

解:

 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 

测试:

 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 

@Sergio Tulentsev的回答是有效的,但是它促进了使用字符串评估的有问题的做法,当输入不是你所期望的时候,这通常充满了安全风险和其他惊喜。 例如,如果一个人打电话给塞尔吉奥的版本会发生什么(不要不尝试):

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

通常可以在没有字符串评估的情况下更仔细地进行ruby元编程。 在这种情况下,使用带有instance_variable_ [get | set]的闭包的简单插值和define_method,并发送:

 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 

这是应该做的。 需要使用class_eval在Class中定义attr_writer。

 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