将实例变量添加到Ruby中的类

时间:2020-03-06 14:54:11  来源:igfitidea点击:

我如何在运行时将实例变量添加到已定义的类中,然后再从类外部获取并设置其值?

我正在寻找一个元编程解决方案,该解决方案允许我在运行时修改类实例,而不是修改最初定义该类的源代码。一些解决方案说明了如何在类定义中声明实例变量,但这不是我要问的。

解决方案

Ruby为此提供了方法,instance_variable_get和instance_variable_set。 (文档)

我们可以创建并分配一个新的实例变量,如下所示:

>> foo = Object.new
=> #<Object:0x2aaaaaacc400>

>> foo.instance_variable_set(:@bar, "baz")
=> "baz"

>> foo.inspect
=> #<Object:0x2aaaaaacc400 @bar=\"baz\">

我们可以使用属性访问器:

class Array
  attr_accessor :var
end

现在我们可以通过以下方式访问它:

array = []
array.var = 123
puts array.var

请注意,我们还可以使用" attr_reader"或者" attr_writer"来仅定义getter或者setter,或者可以按如下方式手动定义它们:

class Array
  attr_reader :getter_only_method
  attr_writer :setter_only_method

  # Manual definitions equivalent to using attr_reader/writer/accessor
  def var
    @var
  end

  def var=(value)
    @var = value
  end
end

如果只想在单个实例上定义它,也可以使用单例方法:

array = []

def array.var
  @var
end

def array.var=(value)
  @var = value
end

array.var = 123
puts array.var

仅供参考,针对对此答案的评论,单例方法很好用,以下是证明:

irb(main):001:0> class A
irb(main):002:1>   attr_accessor :b
irb(main):003:1> end
=> nil
irb(main):004:0> a = A.new
=> #<A:0x7fbb4b0efe58>
irb(main):005:0> a.b = 1
=> 1
irb(main):006:0> a.b
=> 1
irb(main):007:0> def a.setit=(value)
irb(main):008:1>   @b = value
irb(main):009:1> end
=> nil
irb(main):010:0> a.setit = 2
=> 2
irb(main):011:0> a.b
=> 2
irb(main):012:0>

如我们所见,单例方法setit将设置与使用attr_accessor定义的字段相同的字段@b,因此,单例方法是解决此问题的完美方法。

迈克·斯通的答案已经很全面了,但我想补充一点细节。

即使在创建了某个实例之后,我们也可以随时修改类,并获得所需的结果。我们可以在控制台中进行尝试:

s1 = 'string 1'
s2 = 'string 2'

class String
  attr_accessor :my_var
end

s1.my_var = 'comment #1'
s2.my_var = 'comment 2'

puts s1.my_var, s2.my_var

只读,以响应编辑:

Edit: It looks like I need to clarify
  that I'm looking for a metaprogramming
  solution that allows me to modify the
  class instance at runtime instead of
  modifying the source code that
  originally defined the class. A few of
  the solutions explain how to declare
  instance variables in the class
  definitions, but that is not what I am
  asking about. Sorry for the confusion.

我认为我们不太了解"开放课程"的概念,这意味着我们可以随时打开课程。例如:

class A
  def hello
    print "hello "
  end
end

class A
  def world
    puts "world!"
  end
end

a = A.new
a.hello
a.world

上面是完全有效的Ruby代码,这2个类定义可以分布在多个Ruby文件中。我们可以在Module对象中使用" define_method"方法在类实例上定义新方法,但这等效于使用开放类。

Ruby中的"开放类"意味着我们可以在任何时间点重新定义ANY类……这意味着添加新方法,重新定义现有方法或者我们真正想要的任何东西。听起来"开放类"解决方案确实是我们正在寻找的...

其他解决方案也可以很好地工作,但是这里有一个使用define_method的示例,如果我们不愿意使用开放类...它将为数组类定义" var"变量...但是请注意,它是等效的使用开放类...的好处是我们可以对未知类执行此操作(因此,任何对象的类,而不是打开特定的类)... define_method也将在方法内部工作,而我们不能在其中打开类一个方法。

array = []
array.class.send(:define_method, :var) { @var }
array.class.send(:define_method, :var=) { |value| @var = value }

这是一个使用示例。请注意,array2(一个DIFFERENT数组)也具有方法,因此,如果这不是我们想要的,则可能需要单例方法,我在另一篇文章中对此进行了解释。

irb(main):001:0> array = []
=> []
irb(main):002:0> array.class.send(:define_method, :var) { @var }
=> #<Proc:0x00007f289ccb62b0@(irb):2>
irb(main):003:0> array.class.send(:define_method, :var=) { |value| @var = value }
=> #<Proc:0x00007f289cc9fa88@(irb):3>
irb(main):004:0> array.var = 123
=> 123
irb(main):005:0> array.var
=> 123
irb(main):006:0> array2 = []
=> []
irb(main):007:0> array2.var = 321
=> 321
irb(main):008:0> array2.var
=> 321
irb(main):009:0> array.var
=> 123

@只读

如果我们对"类MyObject"的用法是对开放类的用法,那么请注意,我们正在重新定义initialize方法。

在Ruby中,没有重载……只能重载或者重新定义……换句话说,任何给定方法只能有1个实例,因此,如果我们重新定义它,它将被重新定义...并进行初始化方法没有什么不同(即使它是Class对象的新方法所使用的)。

因此,至少在不想访问原始定义的情况下,切勿在没有别名的情况下重新定义现有方法。重新定义未知类的初始化方法可能会很有风险。

无论如何,我认为我为我们提供了一个简单得多的解决方案,该解决方案使用实际的元类来定义单例方法:

m = MyObject.new
metaclass = class << m; self; end
metaclass.send :attr_accessor, :first, :second
m.first = "first"
m.second = "second"
puts m.first, m.second

我们可以同时使用元类和开放类,以使操作更加棘手,并执行以下操作:

class MyObject
  def metaclass
    class << self
      self
    end
  end

  def define_attributes(hash)
    hash.each_pair { |key, value|
      metaclass.send :attr_accessor, key
      send "#{key}=".to_sym, value
    }
  end
end

m = MyObject.new
m.define_attributes({ :first => "first", :second => "second" })

上面的内容基本上是通过"元类"方法公开元类,然后在define_attributes中使用它来通过attr_accessor动态定义一堆属性,然后在哈希中使用关联值调用属性设置器。

使用Ruby,我们可以发挥创造力,并以多种不同方式完成同一件事;-)

仅供参考,如果我们不知道,请按照我的方式使用元类,这意味着我们仅对对象的给定实例进行操作。因此,调用define_attributes将仅为该特定实例定义那些属性。

例子:

m1 = MyObject.new
m2 = MyObject.new
m1.define_attributes({:a => 123, :b => 321})
m2.define_attributes({:c => "abc", :d => "zxy"})
puts m1.a, m1.b, m2.c, m2.d # this will work
m1.c = 5 # this will fail because c= is not defined on m1!
m2.a = 5 # this will fail because a= is not defined on m2!