在Ruby中动态设置本地变量

32

我对在Ruby中动态设置局部变量很感兴趣。不是创建方法、常量或实例变量。

例如:

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

我想要局部变量,因为这个方法存储在一个模型中,我不想破坏全局或对象空间。


3
你需要本地变量而不是简单访问args中的值,有什么特别的原因吗? - dnch
你的意思是在X方法中有一堆变量名和值,然后这些变量在Y方法中作为本地变量使用?此外,这里有Matz的一个有用评论:http://www.ruby-forum.com/topic/155673#685906 - Zabba
基本上,我有一组参数含义不明确的函数,如果传入类似 present_value(:n=>6,:m=>4..etc) 这样的参数,然后在函数中将它们自动作为本地变量使用,而不是通过重新定义或以哈希形式使用它们,会更清晰易懂。跟你的评论 Zabba 类似,我看了一下 https://github.com/maca/arguments,但由于它显然不再得到积极开发支持,因此在 1.9.2 中无法可靠地使用它。 - Allyl Isocyanate
呵呵,这在1.9.2版本中不起作用,我们失去了从活动对象中提取树状结构的功能。你可以用它做一些疯狂的事情。我是作者。 - Macario
Ruby 2.2.2 可能会通过 binding.local_variables 添加此功能。 - Allyl Isocyanate
5个回答

29

补充一下给未来读者的信息,从ruby 2.1.0开始,您可以使用binding.local_variable_getbinding.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

正如文档所述,它的行为类似于

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

你的示例不能直接使用,因为 b.local_variable_set(:b, 3) 会覆盖之前存储在 b 中的绑定。将绑定存储在另一个变量中可以解决这个问题。 - Kelvin
@Kelvin 谢谢,我已经修复了。 - Geoffroy

15

这里的问题在于每个 each_pair 中的代码块具有不同的作用域。在其中分配的局部变量只能在其中访问。例如,以下代码:

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

生成以下内容:

1
2
undefined local variable or method `a' for main:Object (NameError)
为了解决这个问题,你可以创建一个本地的哈希表,为其分配键值,然后在那里访问它们,像这样:
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']

当然,在这个例子中,它只是使用字符串作为键复制了原始哈希表。 我假设实际的用例更加复杂。


2
你是对的。基本上,我想要一种无痛的方法来传递命名参数,然后在方法中使用它们。可惜eval的技巧行不通,我想任何选项都会有糟糕的性能表现。 - Allyl Isocyanate
key = k.to_s 紧跟着 eval('key = v') 没有你期望的效果, key = 1234 有相同的效果。 - Anno2001
eval 不是理想的选择,因为并不是所有东西都可以轻松地序列化为字符串。 - max pleaner

10

有趣的是,您可以更改本地变量,但无法设置它:

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

测试 => 7

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

测试 => NameError:未定义 main:Object的本地变量或方法'x'

这是Dorkus Prime的代码有效的唯一原因。


6

我建议您使用哈希表(但请继续阅读其他备选方案)。

为什么?

允许任意命名参数会导致代码极不稳定。

假设您有一个方法foo,您想要接受这些理论上的命名参数。

情况如下:

  1. 被调用的方法(foo)需要调用一个不带参数的私有方法(我们称之为bar)。如果您向foo传递了一个您想要存储在本地变量bar中的参数,它将掩盖bar方法。解决方法是在调用bar时使用显式括号。

  2. 假设foo的代码分配了一个本地变量。但是,调用者决定传递一个与该本地变量同名的参数。赋值语句将覆盖该参数。

基本上,方法的调用者绝不能改变方法的逻辑。

备选方案

一种中间立场的备选方案涉及OpenStruct。它比使用哈希表少打字。

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

请注意,OpenStruct允许您访问不存在的成员-它将返回nil。如果您想要更严格的版本,请改用Struct
这将创建一个匿名类,然后实例化该类。
h = {:a=>1, :b=>2}
obj = Struct.new(* h.keys).new(* h.values)
obj.a  # => 1
obj.a = 2  # settable
obj.foo  # NoMethodError

1

既然你不想要常量

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

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

puts @b

2

你可能会觉得这种方法很有趣(在正确的上下文中评估变量)

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

4


1
我没有给你点踩,但是楼主说实例变量是不可接受的。这是合理的,因为那会改变对象的状态。 - Kelvin

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