Dynamically set local variables in Ruby [duplicate]

狂风中的少年 提交于 2019-11-27 21:11:48
Dorkus Prime

The problem here is that the block inside each_pair has a different scope. Any local variables assigned therein will only be accessible therein. For instance, this:

args = {}
args[:a] = 1
args[:b] = 2

args.each_pair do |k,v|
  key = k.to_s
  eval('key = v')
  eval('puts key')
end

puts a

Produces this:

1
2
undefined local variable or method `a' for main:Object (NameError)

In order to get around this, you could create a local hash, assign keys to this hash, and access them there, like so:

args = {}
args[:a] = 1
args[:b] = 2

localHash = {}
args.each_pair do |k,v|
  key = k.to_s
  localHash[key] = v
end

puts localHash['a']
puts localHash['b']

Of course, in this example, it's merely copying the original hash with strings for keys. I'm assuming that the actual use-case, though, is more complex.

As an additional information for future readers, starting from ruby 2.1.0 you can using binding.local_variable_get and binding.local_variable_set:

def foo
  a = 1
  b = binding
  b.local_variable_set(:a, 2) # set existing local variable `a'
  b.local_variable_set(:c, 3) # create new local variable `c'
                              # `c' exists only in binding.
  b.local_variable_get(:a) #=> 2
  b.local_variable_get(:c) #=> 3
  p a #=> 2
  p c #=> NameError
end

As stated in the doc, it is a similar behavior to

binding.eval("#{symbol} = #{obj}")
binding.eval("#{symbol}")

interesting, you can change a local variable but you cannot set it:

def test
  x=3
  eval('x=7;')
  puts x
end

test => 7

def test
  eval('x=7;')
  puts x
end

test => NameError: undefined local variable or method `x' for main:Object

This is the only reason why Dorkus Prime's code works.

I suggest you use the hash (but keep reading for other alternatives).

Why?

Allowing arbitrary named arguments makes for extremely unstable code.

Let's say you have a method foo that you want to accept these theoretical named arguments.

Scenarios:

  1. The called method (foo) needs to call a private method (let's call it bar) that takes no arguments. If you pass an argument to foo that you wanted to be stored in local variable bar, it will mask the bar method. The workaround is to have explicit parentheses when calling bar.

  2. Let's say foo's code assigns a local variable. But then the caller decides to pass in an arg with the same name as that local variable. The assign will clobber the argument.

Basically, a method's caller must never be able to alter the logic of the method.

Alternatives

An alternate middle ground involves OpenStruct. It's less typing than using a hash.

require 'ostruct'
os = OpenStruct.new(:a => 1, :b => 2)
os.a  # => 1
os.a = 2  # settable
os.foo  # => nil

Note that OpenStruct allows you access non-existent members - it'll return nil. If you want a stricter version, use Struct instead.

This creates an anonymous class, then instantiates the class.

h = {:a=>1, :b=>2}
obj = Struct.new(* h.keys).new(* h.values)
obj.a  # => 1
obj.a = 2  # settable
obj.foo  # NoMethodError

since you don't want constants

args = {}
args[:a] = 1
args[:b] = 2

args.each_pair{|k,v|eval "@#{k}=#{v};"}

puts @b

2

you might find this approach interesting ( evaluate the variables in the right context)

fn="b*b"
vars=""
args.each_pair{|k,v|vars+="#{k}=#{v};"}
eval vars + fn

4

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!