How to set "dynamically" variable values?
Solution 1
attr_accessor :variable1, :variable2, :variable3
def set_variables(*attributes)
attributes.each {|attribute| self.send("#{attribute}=", true)}
end
Solution 2
Here's the benchmark comparison of send
vs instance_variable_set
:
require 'benchmark'
class Test
VAR_NAME = '@foo'
ATTR_NAME = :foo
attr_accessor ATTR_NAME
def set_by_send i
send("#{ATTR_NAME}=", i)
end
def set_by_instance_variable_set i
instance_variable_set(VAR_NAME, i)
end
end
test = Test.new
Benchmark.bm do |x|
x.report('send ') do
1_000_000.times do |i|
test.set_by_send i
end
end
x.report('instance_variable_set') do
1_000_000.times do |i|
test.set_by_instance_variable_set i
end
end
end
And the timings are:
user system total real
send 1.000000 0.020000 1.020000 ( 1.025247)
instance_variable_set 0.370000 0.000000 0.370000 ( 0.377150)
(measured using 1.9.2)
It should be noted that only in certain situations (like this one, with accessor defined using attr_accessor
) are send
and instance_variable_set
functionally equivalent. If there is some logic in the accessor involved, there would be a difference, and you would have to decide which variant you would need of the two. instance_variable_set
just sets the ivar, while send
actually executes the accessor method, whatever it does.
Another remark - the two methods behave differently in another aspect: if you instance_variable_set
an ivar which doesn't exist yet, it will be created. If you call an accessor which doesn't exist using send
, an exception would be raised.
Solution 3
The method you're after is instance_variable_set
so in your case:
def set_variables(*attributes)
attributes.each {|attribute| self.instance_variable_set(attribute, true)}
end
Solution 4
def set_attributes(*attributes)
attributes.each do |attr|
self.send "#{attr}=", true
end
end
Remember that setter method names end with =
in Ruby.
Solution 5
I know the question was for Rails 3, but the question came up when searching for Rails 4 answers about "how to dynamically access variable values". I tested this on my model and it worked great as an alternative to the proposed solutions:
def set_variables(*attributes)
attributes.each {|attribute| self["#{attribute}"] = true}
end
Backo
Updated on June 06, 2022Comments
-
Backo almost 2 years
I am using Ruby on Rails 3.0.9 and I am trying to set "dynamically" some variable values. That is...
... in my model file I have:
attr_accessor :variable1, :variable2, :variable3 # The 'attributes' argument contains one or more symbols which name is equal to # one or more of the 'attr_accessor' symbols. def set_variables(*attributes) # Here I should set to 'true' all ":variable<N>" attributes passed as symbol # in the 'attributes' array, but variable names should be interpolated in a # string. # # For example, I should set something like "prefix_#{':variable1'.to_s}_suffix". end
How can I set those variable values to
true
?
I tried to use the
self.send(...)
method, but I did not succeed (but, probably, I don't know how to use at all thatsend
method... is it possible do to that I need by using thesend
method?!). -
Marnen Laibow-Koser over 12 yearsI think
send
is the better alternative here. -
Mladen Jablanović over 12 yearsWhy? String interpolation is plain ugly there. Also should be slower as you have additional method calls.
-
Marnen Laibow-Koser over 12 yearsIt won't be any slower, I think. And
instance_variable_set
is messing too much with the object's internals, whereassend
works through the interface.instance_variable_set
is dangerous and rarely a good idea. -
Marnen Laibow-Koser over 11 yearsThe more I think about, the more I think that this merits a downvote for messing with internals.
-
Marnen Laibow-Koser over 11 years@MladenJablanović Rereading your original comment. The idea that there are "additional method calls" is incorrect -- every
send
call in my way of doing it corresponds to aninstance_variable_set
call here. -
Mladen Jablanović over 11 yearsSee the benchmark which I posted as a separate answer. BTW, it was not fair to downvote a perfectly good answer just because you prefer the other way.
-
Marnen Laibow-Koser over 11 years@MladenJablanović I wouldn't have downvoted if it were simply a matter of preference. But this was not a "perfectly good answer". You see,
instance_variable_set
breaks encapsulation by disregarding the object's interface, so it may leave the object in an inconsistent state. Granted, since the receiver isself
, that's less of a problem here, but it's still terrible practice. By contrast,send
uses the object's interface and ensures that the object is in a consistent state.instance_variable_set
is virtually never a good idea in Ruby. -
Marnen Laibow-Koser over 11 years@MladenJablanović Also, you'll have to do string interpolation either way. The attribute names are being passed into the function without leading
@
, which means your sample code won't even work. The correct call would beinstance_variable_set("@#{attribute}", true)
, so your claim that string interpolation is not necessary is false. -
Marnen Laibow-Koser over 11 yearsYou're comparing apples and oranges. As I explained in another comment, the attribute names are coming in without leading
@
, so you'd need to comparesend("#{ATTR_NAME}=", i)
withinstance_variable_set("@#{ATTR_NAME}", i)
to replicate the OP's actual use case. How do those numbers look? -
Marnen Laibow-Koser over 11 yearsI didn't forget. Now, how about an answer to my question?
-
Mladen Jablanović over 11 yearsYour first comment on skorks' answer was that you think
send
was better alternative, and then downvoted it "for messing with internals". For god sake, it is an instance method, its job is to "mess with internals", i.e. change the internal state of the object. Both ways do the job, they change the ivars, so it's OK to prefer one or the other, but it's NOT ok to downvote. So please revert the downvote and then we can continue. -
Marnen Laibow-Koser over 11 yearsYou make a good point about the job of an instance method being to mess with internals. However, I think it's reprehensible for you to hold this discussion hostage to my changing my vote. I'll change my vote if I am convinced to do so by good argumentation; I will not change it solely in order to continue a discussion. That would hurt the integrity of the vote system.
-
Mladen Jablanović over 11 yearsThere's no need arguing NOT downvoting. You would need to provide a credible argument for downvoting. And OP's question is broad enough to allow for more than a single possible solution. We could argue which would be better or more suitable for the particular need. But none of the given answers is neither wrong nor misleading.