Ruby - 关键字参数 - 您可以将所有关键字参数视为哈希吗?怎么样?

时间:2023-01-29 23:20:47

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?

这些关键字参数是否有快捷方式,还是我必须在方法调用中重构我的选项哈希?

4 个解决方案

#1


16  

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.

更新:Ruby 2.2引入了Binding#local_variables,可用于代替Method#参数。要小心,因为在方法中定义任何其他局部变量之前必须调用local_variables。

# 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


7  

Of course! Just use the double splat (**) operator.

当然!只需使用双splat(**)运算符。

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.

它就像常规的splat(*)一样,用于收集参数。您甚至可以将关键字参数转发给另一个方法。

def forward_all(*arguments, **keyword_arguments, &block)
  SomeOtherObject.some_other_method *arguments,
                                    **keyword_arguments,
                                    &block
end

#3


1  

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


0  

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.

要使其工作,请将params视为方法中的保留关键字,并将此行放在方法的顶部。

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

#1


16  

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.

更新:Ruby 2.2引入了Binding#local_variables,可用于代替Method#参数。要小心,因为在方法中定义任何其他局部变量之前必须调用local_variables。

# 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


7  

Of course! Just use the double splat (**) operator.

当然!只需使用双splat(**)运算符。

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.

它就像常规的splat(*)一样,用于收集参数。您甚至可以将关键字参数转发给另一个方法。

def forward_all(*arguments, **keyword_arguments, &block)
  SomeOtherObject.some_other_method *arguments,
                                    **keyword_arguments,
                                    &block
end

#3


1  

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


0  

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.

要使其工作,请将params视为方法中的保留关键字,并将此行放在方法的顶部。

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