【问题标题】:Ruby - Keyword Arguments - Can you treat all of the keyword arguments as a hash? How?Ruby - 关键字参数 - 你能把所有的关键字参数当作一个哈希值吗?如何?
【发布时间】:2014-03-28 09:55:47
【问题描述】:

我有一个看起来像这样的方法:

def method(:name => nil, :color => nil, shoe_size => nil) 
  SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end

对于任何给定的调用,我可以接受可选值的任意组合。我喜欢命名参数,因为我只需查看方法的签名即可查看可用的选项。

我不知道是否有我在上面的代码示例中用大写字母描述的快捷方式。

在过去,它曾经是:

def method(opts)
  SomeOtherObject.some_other_method(opts)
end

优雅,简单,近乎作弊。

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

【问题讨论】:

    标签: ruby hash arguments keyword options


    【解决方案1】:

    是的,这可能的,但不是很优雅。

    您必须使用the parameters method,它返回方法参数及其类型的数组(在这种情况下,我们只有关键字参数)。

    def foo(one: 1, two: 2, three: 3)
      method(__method__).parameters
    end  
    #=> [[:key, :one], [:key, :two], [:key, :three]]
    

    知道这一点后,有多种方法可以使用该数组来获取所有参数及其提供的值的哈希值。

    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}
    

    所以你的例子看起来像

    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
    

    仔细考虑使用它。它很聪明,但以可读性为代价,其他阅读您的代码的人不会喜欢它。

    您可以使用辅助方法使其更具可读性。

    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
    

    更新: Ruby 2.2 引入了Binding#local_variables,可以用来代替Method#parameters。请小心,因为在方法内定义任何其他局部变量之前,您必须调用 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}
    

    【讨论】:

    • 这个问题我们不想重述如下? def foo(one: 1, two: 2, three: 3) params = {一: 一, 二: 二, 三: 三)} end 像你的例子那样写出来有什么好处?这似乎令人困惑。还有一个问题是这些实施的合法性如何?这是使用 named_pa​​rams 的好方法吗?
    • @jmoon90 没错,我们不想做params = { ... },因为那时我们对实现进行了硬编码,它变得非常耦合。像在我的示例中那样做的好处是您可以更改方法签名并仍然自动捕获所有关键字参数。我不确定我是否理解您关于named_params 的其他问题。
    • 在使用local_variables时最好使用binding.local_variable_get(p)而不是eval(p.to_s),只是为了避免这种邪恶eval ...
    【解决方案2】:

    当然!只需使用双 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"}
    

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

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

    【讨论】:

    • 不,我有兴趣将所有可选的命名关键字参数收集到一个哈希中。我不是想创建一个新的选项哈希。我想要一个 {:name => val, :color => val, etc.} 的哈希,它们在方法签名中命名。
    • 无论您对什么感兴趣,这都是迄今为止最好的答案。
    【解决方案3】:

    我玩得很开心,所以谢谢你。这是我想出的:

    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
    

    我选择返回一个数组,但您可以轻松修改它以返回一个哈希值(例如,在初始检测后不关心参数类型)。

    【讨论】:

      【解决方案4】:

      下面的语法怎么样?

      要使其正常工作,请将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
      

      【讨论】:

      • 为什么reject变量以_开头?
      • 根据 Ruby 样式指南,建议使用带有前导下划线的参数名称来指定将在方法或块中不使用的参数。事实上,Ruby 对待名为_ 的变量不同于其他变量。有关更多信息,请参阅po-ru.com/diary/rubys-magic-underscore。 Abdo 解决方案中的那一行只是将它们过滤掉,因此它们不会被传递。
      【解决方案5】:

      @Dennis 的回答很有用且具有教育意义。但是,我注意到 Binding#local_variables 将返回 all 局部变量,无论何时执行 local_variables

      def locals_from_binding(binding_:)
        binding_.local_variables.map { |var|
          [var, binding_.local_variable_get(var)]
        }.to_h
      end
      
      def m(a:, b:, c:)
        args = locals_from_binding(binding_: binding)
        pp args
      
        d = 4
      end
      
      m(a: 1, b: 3, c: 5)
      # Prints:
      #   {:a=>1, :b=>3, :c=>5, :args=>nil, :d=>nil}
      # Note the presence of :d
      

      我提出一个混合解决方案:

      def method_args_from_parameters(binding_:)
        method(caller_locations[0].label)
        .parameters.map(&:last)
        .map { |var|
          [var, binding_.local_variable_get(var)]
        }.to_h
      end
      
      def m(a:, b:, c:)
        args = method_args_from_parameters(binding_: binding)
        pp args
      
        d = 4
      end
      
      m(a: 1, b: 3, c: 5)
      # Prints:
      #   {:a=>1, :b=>3, :c=>5}
      # Note the absence of :d
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-16
        • 2017-05-07
        • 2014-08-30
        • 2017-01-27
        • 1970-01-01
        • 2020-03-25
        相关资源
        最近更新 更多