具有可选的第一个哈希参数和 keyword_args 的奇怪方法行为

Odd method behaviour with optional first hash parameter and keyword_args

我有以下方法:

def test(first_param = nil, keyword_arg: nil)
  puts "first_param: #{first_param}"
  puts "keyword_arg: #{keyword_arg}"
end

以下所有调用都按照我的预期进行:

test(:something)
#=> first_param: something
#   keyword_arg:

test(nil, keyword_arg: :keyword_arg)
#=> first_param:
#   keyword_arg: keyword_arg

test({ first_param: :is_a_hash }, keyword_arg: :is_still_working)
#=> first_param: {:first_param=>:is_a_hash}
#   keyword_arg: is_still_working

但是省略可选的 keyword_arg 并将散列作为第一个参数传递给我一个错误:

test(first_param: :is_a_hash)
#=> test.rb:1:in `test': unknown keyword: first_param (ArgumentError)
#           from test.rb:12:in `<main>'

我希望它将 first_param 设置为 { first_param: :is_hash } 并且 keyword_argnil

它似乎将每个散列解释为关键字 arg:

test(keyword_arg: :should_be_first_param)
#=> first_param:
#   keyword_arg: should_be_first_param

我认为这应该将 first_param 设置为 { keyword_arg: :should_be_first_param },留下 keyword_arg nil

这是解析器错误还是预期行为?在 ruby 2.3.0 和 2.2.4.

上测试

编辑:将第一个参数设置为强制性,一切都像我期望的那样:

def test_mandatory(first_param, keyword_arg: nil)
  puts "first_param: #{first_param}"
  puts "keyword_arg: #{keyword_arg}"
end

test_mandatory(first_param: :is_a_hash)
#=> first_param: {:first_param=>:is_a_hash}
#   keyword_arg:

test_mandatory(keyword_arg: :should_be_first_param)
#=> first_param: {:keyword_arg=>:should_be_first_param}
#   keyword_arg:

我希望将参数设为可选不会改变参数的解析方式。

我打开了一个 issue on bugs.ruby-lang.org,然后开发人员可以弄清楚它是这样设计的还是 kword args 的副作用。

根据Marc-Andre Lafortune's reply预计:

This behavior may be surprising but it is intentional.

It boils down to giving priority to filling keyword arguments first instead of filling unnamed parameters. It is actually the only possible way to go. Among other things, think about the following example:

def foo(*rest, bar: 42)
end

If we don't prioritize named arguments first, then there is simply no way to specify a value for bar in this example!

So Ruby checks that:

  • after all mandatory unnamed arguments are filled
  • if the last remaining argument is hash-like
  • and all its keys are symbols
  • and the method called uses keyword arguments

=> then that parameter is used for keyword arguments.

Note the requirement on keys being symbols. This can yield even more surprising if you pass a hash with some keys that are not symbols:

def foo(a = nil, b: nil)
  p a, b
end
foo(:b  => 42) # => nil, 42
foo('b' => 42) # => {"b" => 42}, nil