问题
I have a method that looks like this:
def method(:name => nil, :color => nil, shoe_size => nil)
SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end
For any given call, I can accept any combination of optional values. I like the named arguments, because I can just look at the method's signature to see what options are available.
What I don't know is if there is a shortcut for what I have described in capital letters in the code sample above.
Back in the olden days, it used to be:
def method(opts)
SomeOtherObject.some_other_method(opts)
end
Elegant, simple, almost cheating.
Is there a shortcut for those Keyword Arguments or do I have to reconstitute my options hash in the method call?
回答1:
Yes, this is possible, but it's not very elegant.
You'll have to use the parameters method, which returns an array of the method's parameters and their types (in this case we only have keyword arguments).
def foo(one: 1, two: 2, three: 3)
method(__method__).parameters
end
#=> [[:key, :one], [:key, :two], [:key, :three]]
Knowing that, there's various ways how to use that array to get a hash of all the parameters and their provided values.
def foo(one: 1, two: 2, three: 3)
params = method(__method__).parameters.map(&:last)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
So your example would look like
def method(name: nil, color: nil, shoe_size: nil)
opts = method(__method__).parameters.map(&:last).map { |p| [p, eval(p.to_s)] }.to_h
SomeOtherObject.some_other_method(opts)
end
Think carefully about using this. It's clever but at the cost of readability, others reading your code won't like it.
You can make it slightly more readable with a helper method.
def params # Returns the parameters of the caller method.
caller_method = caller_locations(length=1).first.label
method(caller_method).parameters
end
def method(name: nil, color: nil, shoe_size: nil)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
SomeOtherObject.some_other_method(opts)
end
Update: Ruby 2.2 introduced Binding#local_variables
which can be used instead of Method#parameters
. Be careful because you have to call local_variables
before defining any additional local variables inside the method.
# Using Method#parameters
def foo(one: 1, two: 2, three: 3)
params = method(__method__).parameters.map(&:last)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
# Using Binding#local_variables (Ruby 2.2+)
def bar(one: 1, two: 2, three: 3)
binding.local_variables.params.map { |p|
[p, binding.local_variable_get(p)]
}.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
回答2:
Of course! Just use the double splat (**
) operator.
def print_all(**keyword_arguments)
puts keyword_arguments
end
def mixed_signature(some: 'option', **rest)
puts some
puts rest
end
print_all example: 'double splat (**)', arbitrary: 'keyword arguments'
# {:example=>"double splat (**)", :arbitrary=>"keyword arguments"}
mixed_signature another: 'option'
# option
# {:another=>"option"}
It works just like the regular splat (*
), used for collecting parameters. You can even forward the keyword arguments to another method.
def forward_all(*arguments, **keyword_arguments, &block)
SomeOtherObject.some_other_method *arguments,
**keyword_arguments,
&block
end
回答3:
I had some fun with this, so thanks for that. Here's what I came up with:
describe "Argument Extraction Experiment" do
let(:experiment_class) do
Class.new do
def method_with_mixed_args(one, two = 2, three:, four: 4)
extract_args(binding)
end
def method_with_named_args(one:, two: 2, three: 3)
extract_named_args(binding)
end
def method_with_unnamed_args(one, two = 2, three = 3)
extract_unnamed_args(binding)
end
private
def extract_args(env, depth = 1)
caller_param_names = method(caller_locations(depth).first.label).parameters
caller_param_names.map do |(arg_type,arg_name)|
{ name: arg_name, value: eval(arg_name.to_s, env), type: arg_type }
end
end
def extract_named_args(env)
extract_args(env, 2).select {|arg| [:key, :keyreq].include?(arg[:type]) }
end
def extract_unnamed_args(env)
extract_args(env, 2).select {|arg| [:opt, :req].include?(arg[:type]) }
end
end
end
describe "#method_with_mixed_args" do
subject { experiment_class.new.method_with_mixed_args("uno", three: 3) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: "uno", type: :req },
{ name: :two, value: 2, type: :opt },
{ name: :three, value: 3, type: :keyreq },
{ name: :four, value: 4, type: :key }
])
end
end
describe "#method_with_named_args" do
subject { experiment_class.new.method_with_named_args(one: "one", two: 4) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: "one", type: :keyreq },
{ name: :two, value: 4, type: :key },
{ name: :three, value: 3, type: :key }
])
end
end
describe "#method_with_unnamed_args" do
subject { experiment_class.new.method_with_unnamed_args(2, 4, 6) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: 2, type: :req },
{ name: :two, value: 4, type: :opt },
{ name: :three, value: 6, type: :opt }
])
end
end
end
I chose to return an array, but you could easily modify this to return a hash instead (for instance, by not caring about the argument type after the initial detection).
回答4:
How about the syntax below?
For it to work, treat params
as a reserved keyword in your method and place this line at the top of the method.
def method(:name => nil, :color => nil, shoe_size => nil)
params = params(binding)
# params now contains the hash you're looking for
end
class Object
def params(parent_binding)
params = parent_binding.local_variables.reject { |s| s.to_s.start_with?('_') || s == :params }.map(&:to_sym)
return params.map { |p| [ p, parent_binding.local_variable_get(p) ] }.to_h
end
end
来源:https://stackoverflow.com/questions/22026694/ruby-keyword-arguments-can-you-treat-all-of-the-keyword-arguments-as-a-hash