在Ruby模块中动态创建访问器

时间:2014-07-25 01:21:28

标签: ruby metaprogramming

我在Ruby中有一个Config模块,我希望能够添加任意变量。我使用method_missing和instance_variable_set创建了它,如下所示:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
  end
end

但是,我在动态创建访问器方面遇到了麻烦。当我尝试使用attr_accessor时如下:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
    module_eval("attr_accessor :#{m}")
  end
end

我得到以下内容:

Conf :: s3_key(' 1234ABC')#Conf :: s3_key = nil

如果我尝试单独创建访问者:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)
    module_eval("def self.#{m};@#{m};end")
    module_eval("def self.#{m}=(val);@#{m}=val;end")
  end
end

发生以下情况:

Conf::s3_key('1234ABC') # Conf::s3_key='1234ABC' - correct

但如果我尝试覆盖该值,我会收到错误

Conf::s3_key('1234ABC') # ok
Conf::s3_key('4567DEF') #(eval):1:in `s3_key': wrong number of arguments (1 for 0) (ArgumentError)

我做错了什么?

2 个答案:

答案 0 :(得分:2)

首先,即使正常描述,attr_accessor也无法用于模块。

module Conf
  attr_accessor :s3_key
end

其次,覆盖错误是因为method_missing只执行一次

  def self.method_missing(m, *args)
    #:
    instance_variable_set("@#{m}", args)
    module_eval("def self.#{m};@#{m};end") # <- method defined

该方法在第一次调用中定义。 参数的数量为0

Conf::s3_key('1234ABC') # call method_missing
Conf::s3_key('4567DEF') # call self.s3_key()

例如,这样怎么样:

module Conf
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)
    module_eval(<<EOS)
def self.#{m}(*args)
  if (args.empty?)
    @#{m}
  else
    @#{m} = (args.length==1) ? args.pop : args
  end
end
EOS
  end
end

Conf::s3_key('foo')
Conf::s3_key('bar')
p Conf::s3_key                   # "bar"

或者

module Conf
  def self.method_missing(m, *args)
    if (m.to_s =~ /^(.+)=$/)
      args = args.pop if args.length==1
      instance_variable_set("@#{$1}", args)
    else
      instance_variable_get("@#{m}")
    end
  end
end

Conf::s3_key = 'foo'
Conf::s3_key = 'bar'
p Conf::s3_key                   # "bar"

答案 1 :(得分:1)

您只需要更改代码的一行。

<强>代码

module Conf
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
    Module.instance_eval("attr_accessor :#{m}")
  end
end

示例

Conf.s3_key('1234ABC')
Conf.s3_key             #=> "1234ABC"
Conf.s3_key = '4567DEF'
Conf.s3_key             #=> "4567DEF"

(或Conf::s3_key('1234ABC')等)

<强>解释

为类定义访问器并应用于类实例。在这种情况下,类实例是模块Conf,因此必须为attr_accessor是实例的类定义Conf

Conf.class #=> Module

请注意

Module.is_a? Class       #=> true
Conf.instance_of? Module #=> true

我们通过在Module上调用BasicObject#instance_eval来执行此操作。我们需要使用instance_eval,以便变量m在调用时将在范围内。

最后一次观察。假设模块Conf被另一个模块M包围。然后代码仍然有效:

   M::Conf.s3_key('1234ABC')
   M::Conf.s3_key             #=> "1234ABC"
   M::Conf.s3_key = '4567DEF'
   M::Conf.s3_key             #=> "4567DEF"

那是因为所有模块,包括嵌套的模块,都是类Module的实例。