在Ruby中将参数命名为局部变量

时间:2010-01-07 23:32:58

标签: ruby variables arguments

在使用方法的命名参数时,我发现自己经常在Ruby中编写我认为不必要的代码。

以下面的代码为例:

def my_method(args)
  orange = args[:orange]
  lemon = args[:lemon]
  grapefruit = args[:grapefruit]

  # code that uses 
  # orange, lemon & grapefruit in this format which is way prettier & concise than 
  # args[:orange] args[:lemon] args[:grapefruit]

  puts "my_method variables: #{orange}, #{lemon}, #{grapefruit}" 
end
my_method :orange => "Orange", :grapefruit => "Grapefruit"

我真的不喜欢这段代码,我不得不接受args并将值传递给局部变量,这些变量与DRY原则相反,并且通常占用我的方法中的空间。如果我不使用局部变量并且只使用args [:symbol]语法引用所有变量,那么代码就会变得难以理解。

我已经尝试过解决这个问题,但是我不知道如何在方法范围内使用eval或使用任何其他技术来定义局部变量。以下是下面的许多尝试之一,这会导致错误

def my_method_with_eval(args)
  method_binding = binding
  %w{ orange lemon grapefruit}.each { |variable| eval "#{variable} = args[:#{variable}]", method_binding; }

  # code that uses 
  # orange, lemon & grapefruit in this format which is way prettier & concise than 
  # args[:orange] args[:lemon] args[:grapefruit]

  puts "my_method_with_eval variables: #{orange}, #{lemon}, #{grapefruit}" 
end
my_method_with_eval :orange => "Orange", :grapefruit => "Grapefruit"

运行该代码时,我只需

NameError: undefined local variable or method ‘orange’ for main:Object method my_method_with_eval in named_args_to_local_vars at line at top level in named_args_to_local_vars at line 9

任何人都有任何想法如何以某种方式简化此操作,以便我不必使用var = args [:var]代码的负载启动我的命名参数方法?

谢谢, Matthew O'Riordan

6 个答案:

答案 0 :(得分:6)

我不相信在Ruby中有任何方法可以做到这一点(如果有人想出一个,请告诉我,我会更新或删除这个答案以反映它!) - 如果一个局部变量没有'尚未定义,没有办法用绑定动态定义它。你可以想象在调用eval之前做orange, lemon, grapefruit = nil这样的事情,但是你可能遇到其他问题 - 例如,如果args [:orange]是字符串“Orange”,你最终会评估orange = Orange与您当前的实施。

虽然使用标准库中的OpenStruct类(“可以工作”,但我的意思是“这取决于你的风格,a.orange是否比任何更好args[:orange]“):

require 'ostruct'

def my_method_with_ostruct(args)
  a = OpenStruct.new(args)
  puts "my_method_with_ostruct variables: #{a.orange}, #{a.lemon}, #{a.grapefruit}"
end

如果您不需要轻松访问此方法接收器上的任何状态或方法,则可以使用instance_eval,如下所示。

def my_method_with_instance_eval(args)
  OpenStruct.new(args).instance_eval do
    puts "my_method_with_instance_eval variables: #{orange}, #{lemon}, #{grapefruit}"
  end
end

您甚至可以something tricky with method_missing(有关详情,请参阅here)以允许访问“主要”对象,但效果可能不会很好。

总而言之,我认为可能最简单/可读的方法是使用较少干扰你的DRY初始解决方案。

答案 1 :(得分:6)

Greg和Sand的答案合并:

require 'ostruct'

def my_method(args = {}) 
  with args do
    puts a
    puts b
  end 
end

def with(args = {}, &block)
  OpenStruct.new(args).instance_eval(&block)
end

my_method(:a => 1, :b => 2)

答案 2 :(得分:3)

我在ruby-talk-google上找到了关于此的讨论,它似乎是解析器的优化。局部变量已在运行时计算出来,因此local_variables已在方法的开头设置。

def meth
  p local_variables
  a = 0
  p local_variables
end
meth
# =>
[:a]
[:a]

这样Ruby就不需要在运行时决定a是方法还是局部变量,或者可以安全地假设它是局部变量。

(为了比较:在Python locals()中,在函数的开头会为空。)

答案 3 :(得分:1)

在我的博客上(请参阅用户信息中的链接),我只是试图解决这个问题。我在那里详细介绍,但我的解决方案的核心是以下帮助方法:

def collect_named_args(given, expected)
    # collect any given arguments that were unexpected
    bad = given.keys - expected.keys

    # if we have any unexpected arguments, raise an exception.
    # Example error string: "unknown arguments sonething, anyhting"
    raise ArgumentError,
        "unknown argument#{bad.count > 1 ? 's' : ''}: #{bad.join(', ')}",
        caller unless bad.empty?

    Struct.new(*expected.keys).new(
        *expected.map { |arg, default_value| 
            given.has_key?(arg) ? given[arg] : default_value
        }
    )
end # def collect_named_args

的调用如下:

def foo(arguments = {})
    a = collect_named_args(arguments,
        something:  'nothing',
        everything: 'almost',
        nothing:     false,
        anything:    75)

    # Do something with the arguments 
    puts a.anything
end # def foo

我仍在试图弄清楚是否有办法将我的结果变成local_variables - 但正如其他人所说,Ruby不想这样做。我想你可以使用“with”技巧。

module Kernel
  def with(object, &block)
    object.instance_eval &block
  end
end

然后

with(a) do
  # Do something with arguments (a)
  put anything
end

但由于几个原因,这感觉不尽如人意。

我喜欢上面的解决方案,因为它使用的是Struct而不是OpenStruct,这意味着需要的更少,并且只要处理哪些变量,就会设置回来的内容。

答案 4 :(得分:0)

这不能解决问题,但我倾向于

orange, lemon, grapefruit = [:orange, :lemon, :grapefruit].
map{|key| args.fetch(key)}

因为复制并粘贴orange lemon grapefruit位非常容易。

如果你发现冒号过多,你可以做

orange, lemon, grapefruit = %w{orange, lemon, grapefruit}.
map{|str| str.gsub(",", "").to_sym}.map{|key| args.fetch(key)}

答案 5 :(得分:0)

我发现自己想知道今天如何做到这一点。我不仅想要干掉我的代码,而且我也希望进行参数验证。

我遇到了a blog post by Juris Galang,他在那里解释了几种处理它的方法。他发表的a gem that encapsulates his ideas看起来很有趣。