Dynamically set local variables in Ruby [duplicate]

这一生的挚爱 提交于 2019-11-26 23:00:55

问题


This question already has an answer here:

  • How to dynamically create a local variable? 4 answers

I'm interested in dynamically setting local variables in Ruby. Not creating methods, constants, or instance variables.

So something like:

args[:a] = 1
args.each_pair do |k,v|
  Object.make_instance_var k,v
end
puts a
> 1

I want locally variables specifically because the method in question lives in a model and I dont want to pollute the global or object space.


回答1:


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.




回答2:


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}")



回答3:


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.




回答4:


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



回答5:


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



来源:https://stackoverflow.com/questions/4963678/dynamically-set-local-variables-in-ruby

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