如何为动态实例变量设置attr_accessor?

33

我在类中动态创建了一个实例变量:

class Mine
  attr_accessor :some_var

  def intialize
    @some_var = true
  end

  def my_number num
    self.instance_variable_set "@my_#{num}", num
  end
end

如何将@my_#{num}现在作为属性值?

例如,我想要能够做到这一点:

dude = Mine.new
dude.my_number 1
dude.my_1
=> 1
7个回答

36

这个答案不会污染类空间,比如说如果我执行 mine.my_number 4,其他的 Mine 实例将不会获得 my_4 方法.. 这是因为我们使用了对象的单例类而不是类本身。

class Mine
  def my_number num
    singleton_class.class_eval { attr_accessor "my_#{num}" }
    send("my_#{num}=", num)
  end
end

a = Mine.new
b = Mine.new
a.my_number 10 #=> 10
a.my_10 #=> 10
b.my_10 #=> NoMethodError

使用singleton_class有什么理由吗?同时,作为一种单行写法,class_eval { attr_accessor:"my_#{num}" }语法会更清晰 :) - Halil Özgür
2
@HalilÖzgür你希望只对该实例定义该方法,这就是为什么你使用singleton_class的原因,如果你使用self.class,那么所有实例都会获得该方法,而你并不想要那样。你对语法的理解是正确的,我会进行更改。 - Orlando
顺便提一下,singleton_class 和 class << self 是相同的。 - Orlando
我的意思是,即使没有使用“singleton_class”,上述代码也会产生相同的输出。 - Halil Özgür
1
@HalilÖzgür class_eval 是一个类方法,而不是实例方法。 - Orlando
哦,抱歉!我把另一件事情和这个混淆了,有趣的是它似乎一度显示相同的结果。 - Halil Özgür

25

可以使用 __send__ 来实现。这里:

class Mine
  attr_accessor :some_var

  def intialize
    @some_var = true
  end

  def my_number num
    self.class.__send__(:attr_accessor, "my_#{num}")
    self.__send__("my_#{num}=", num)
  end
end

dude = Mine.new
dude.my_number 1
puts dude.my_1

=> 1

3
这将为所有实例定义访问器,而不仅仅是您在 my_number 上调用的那一个。我添加了一个额外的答案,只为您为其添加了实例变量的实例添加方法。 - carpeliam

10

很简单。你可以在my_number方法内动态定义属性读取器:

  def my_number num
     self.instance_variable_set "@my_#{num}", num
     self.class.class_eval do
        define_method("my_#{num}") { num }
     end
  end

看看那是否适合你


5

你可以使用OpenStruct:

require "ostruct"

class Mine < OpenStruct
end

dude = Mine.new
dude.my_number = 1
dude.my_number # => 1

我不知道为什么你想让dude.my_1返回1——这不是把你已经拥有的东西还给你吗?


我很好奇你如何同时继承OpenStruct和ActiveRecord::Base。 - Trip
1
@Trip,你不能从多个类继承。 - Andrew Grimm

4

两种方法中存在一个问题... 如果在一个实例中设置了一个实例变量,其访问器将对所有实例都可用,因为你是在self.class而不是self上定义方法。

dude = Mine.new
dude.my_number 1
puts dude.my_1
dudette = Mine.new
dudette.my_1 = 2    # works, but probably shouldn't
dudette.my_number 2
dude.my_2 = 3       # works, but probably shouldn't

你可能想要做的是仅修改具有实例变量的实例:
class Mine
  # ...
  def my_number num
    class << self
      attr_accessor "my_#{num}"
    end
    self.send("my_#{num}=", num)
  end
end

这样一来,实例变量只会在它们所属的对象上获取访问器。我也没有使用instance_variable_set,因为如果您正在设置一个访问器,那么我认为最好是重用它。但这是一种风格选择。重要的是在调用而不是。


当我尝试这个时,我得到了错误信息:“undefined local variable or method `num' for #<Class:#Mine:...”。看起来num变量在这里超出了作用域。 - kinofrost
啊,你说得对!我需要再考虑一下。同时,这不是正确的解决方案。:/ - carpeliam
2
我也试过了,不行。如果你的答案不起作用,就把它删除掉。 - davidmontoyago

0

又一个解决方案可以添加到堆栈中,define_singleton_method

class Mine
  def my_number num
    define_singleton_method("num_#{num}") { num }
  end
end

所有这些解决方案的一个副作用是,如果您多次使用不同的数字调用它,您最终会在对象上拥有一堆方法:

dude = Mine.new
dude.my_number 1
dude.my_number 5
dude.my_1
=> 1
dude.my_5
=> 5

我们可以通过删除旧的方法来修复这个问题:
class Mine
  def my_number num
    old_num = @num
    if @num
      # need to use `old_num` local variable
      # instance var scope is different inside `class_eval`
      singleton_class.class_eval { remove_method("num_#{old_num}") }
    end

    @num = num

    define_singleton_method("num_#{num}") { @num }
  end
end

-1
较旧的线程,但我发现它很有用,谢谢。这是Dorkus Prime的答案代码,同时还从哈希中获取名称\值的实例变量。
@cookies = browser.cookies.to_a

@cookies.each do |cookie|
self.class.__send__(:attr_accessor, "#{cookie[:name]}")
self.__send__("#{cookie[:name]}=",cookie[:value])
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接