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