命名参数作为 Ruby 中的局部变量

Posted

技术标签:

【中文标题】命名参数作为 Ruby 中的局部变量【英文标题】:Named arguments as local variables in Ruby 【发布时间】:2011-01-02 17:29:13 【问题描述】:

在为方法使用命名参数时,我发现自己经常在 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

【问题讨论】:

听起来你真正想要的是命名参数。这些存在于 Ruby 2.0 中。 【参考方案1】:

我不相信在 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)来允许访问“主要”对象,但性能可能不会很好。

总而言之,我认为使用困扰您的不太干燥的初始解决方案可能是最直接/最易读的。

【讨论】:

感谢您的意见。在尝试找到解决方案几天之后,我认为您的建议是保持它现在的样子可能是最好的!尝试将变量添加到 local_variables 似乎非常困难,而且 Ruby 也不鼓励这样做。我确实看到曾经有一个扩展 Binding.caller(在 extensions.rubyforge.org/rdoc/index.html 中),它可以提供获取调用绑定和插入局部变量的功能,但已被弃用。【参考方案2】:

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)

【讨论】:

这似乎是一个非常漂亮的解决方案,并且可能非常有用 美丽优雅的方法。效果很好。感谢分享!【参考方案3】:

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

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

这样,Ruby 不需要在运行时判断 a 是方法还是局部变量,但可以安全地假定它是局部变量。

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

【讨论】:

【参考方案4】:

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

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,这意味着少了一个需求,而且你得到的东西是根据正在处理的变量来设置的。

【讨论】:

【参考方案5】:

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

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

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

如果你觉得冒号的工作量太大,你可以这样做

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

【讨论】:

【参考方案6】:

我发现自己今天想知道如何自己做这件事。我不仅想干掉我的代码,还想进行参数验证。

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

【讨论】:

以上是关于命名参数作为 Ruby 中的局部变量的主要内容,如果未能解决你的问题,请参考以下文章

默认形参和关键字实参,收集参数,命名关键字参数,return自定义返回,全局变量和局部变量,函数名的使用

默认形参和关键字实参,收集参数,命名关键字参数,return自定义返回,全局变量和局部变量,函数名的使用

Ruby 中没有命名参数?

何时在 Ruby 中使用关键字参数,也就是命名参数

我可以在 Ruby 2.x 中要求命名参数吗?

为啥不能在 Ruby 3 中结合 `...` 和命名参数?