【问题标题】:Named Parameters in Ruby StructsRuby 结构中的命名参数
【发布时间】:2011-07-21 10:41:42
【问题描述】:

我对 Ruby 很陌生,如果这是一个明显的问题,我深表歉意。

我想在实例化 Struct 时使用命名参数,即能够指定 Struct 中的哪些项获取哪些值,并将其余项默认为 nil。

例如我想做的:

Movie = Struct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'

这不起作用。

所以我想出了以下内容:

class MyStruct < Struct
  # Override the initialize to handle hashes of named parameters
  def initialize *args
    if (args.length == 1 and args.first.instance_of? Hash) then
      args.first.each_pair do |k, v|
        if members.include? k then
          self[k] = v
        end
      end
    else
      super *args
    end
  end
end

Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'

这似乎工作得很好,但我不确定是否有更好的方法来做到这一点,或者我是否正在做一些非常疯狂的事情。如果有人可以验证/破解这种方法,我将不胜感激。

更新

我最初在 1.9.2 中运行它,它工作正常;但是,在其他版本的 Ruby 中尝试过(谢谢 rvm),它的工作/不工作如下:

  • 1.8.7:不工作
  • 1.9.1:工作
  • 1.9.2:工作
  • JRuby(设置为 1.9.2 运行):不工作

JRuby 对我来说是个问题,因为我想让它与它保持兼容以用于部署目的。

还有一个更新

在这个不断增加的杂乱无章的问题中,我尝试了各种版本的 Ruby,发现 1.9.x 中的结构将其成员存储为符号,但在 1.8.7 和 JRuby 中,它们存储为字符串,所以我更新了代码如下(接受已经给出的建议):

class MyStruct < Struct
  # Override the initialize to handle hashes of named parameters
  def initialize *args
    return super unless (args.length == 1 and args.first.instance_of? Hash)
    args.first.each_pair do |k, v|
      self[k] = v if members.map {|x| x.intern}.include? k
    end
  end
end

Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'

现在这似乎适用于我尝试过的所有 Ruby 风格。

【问题讨论】:

  • 你的代码看起来不错,真的。
  • 我一直在寻找同样的东西 - 是否有一个标准的 gem 可以做到这一点,以及指定哪些参数是必需的?
  • github.com/chemica/solid-struct 执行此操作,但不强制执行必需的参数。

标签: ruby struct named-parameters


【解决方案1】:

综合现有答案揭示了一个更简单的 Ruby 2.0+ 选项:

class KeywordStruct < Struct
  def initialize(**kwargs)
    super(*members.map{|k| kwargs[k] })
  end
end

用法与现有的Struct 相同,其中任何未给出的参数将默认为nil

Pet = KeywordStruct.new(:animal, :name)
Pet.new(animal: "Horse", name: "Bucephalus") # => #<struct Pet animal="Horse", name="Bucephalus">  
Pet.new(name: "Bob") # => #<struct Pet animal=nil, name="Bob"> 

如果你想要像 Ruby 2.1+ 的 required kwargs 这样的参数,这是一个非常小的变化:

class RequiredKeywordStruct < Struct
  def initialize(**kwargs)
    super(*members.map{|k| kwargs.fetch(k) })
  end
end

此时,覆盖 initialize 以提供某些 kwargs 默认值也是可行的:

Pet = RequiredKeywordStruct.new(:animal, :name) do
  def initialize(animal: "Cat", **args)
    super(**args.merge(animal: animal))
  end
end

Pet.new(name: "Bob") # => #<struct Pet animal="Cat", name="Bob">

【讨论】:

  • 有趣。我在您的实现上运行了 benchmark/ips,它比简单地实现 Pet 对象而不是结构慢大约 2 倍。 Obj 大约快 2 倍。但是,您的 Keyword Struct 仍然比 Openstruct 快大约 3 倍,因此如果您不需要这种灵活性,它是一个可行的选择。
【解决方案2】:

对于较新版本的 Ruby,您可以使用 keyword_init: true

Movie = Struct.new(:title, :length, :rating, keyword_init: true)

Movie.new(title: 'Title', length: '120m', rating: 'R')
  # => #<struct Movie title="Title", length="120m", rating="R">

【讨论】:

    【解决方案3】:

    你知道的越少越好。无需知道底层数据结构是使用符号还是字符串,甚至是否可以寻址为Hash。只需使用属性设置器:

    class KwStruct < Struct.new(:qwer, :asdf, :zxcv)
      def initialize *args
        opts = args.last.is_a?(Hash) ? args.pop : Hash.new
        super *args
        opts.each_pair do |k, v|
          self.send "#{k}=", v
        end
      end
    end
    

    它接受位置参数和关键字参数:

    > KwStruct.new "q", :zxcv => "z"
     => #<struct KwStruct qwer="q", asdf=nil, zxcv="z">
    

    【讨论】:

    • 红宝石魔法。谢谢我喜欢这个。
    • 非常聪明的答案!在 Ruby 1.9 和 2.x 中测试和工作
    【解决方案4】:

    允许 Ruby 关键字参数 (Ruby >=2.0) 的解决方案。

    class KeywordStruct < Struct
      def initialize(**kwargs)
        super(kwargs.keys)
        kwargs.each { |k, v| self[k] = v }
      end
    end
    

    用法:

    class Foo < KeywordStruct.new(:bar, :baz, :qux)
    end
    
    
    foo = Foo.new(bar: 123, baz: true)
    foo.bar  # --> 123
    foo.baz  # --> true
    foo.qux  # --> nil
    foo.fake # --> NoMethodError
    

    这种结构作为值对象非常有用,特别是如果您喜欢更严格的方法访问器,它实际上会出错而不是返回nil(一个la OpenStruct)。

    【讨论】:

    • 这个解决方案在给出零参数时失败:foo = Foo.new() 得到你&lt;struct Foo bar=[], baz=nil, qux=nil&gt;。请改用@indirect 的answer
    【解决方案5】:

    你考虑过 OpenStruct 吗?

    require 'ostruct'
    
    person = OpenStruct.new(:name => "John", :age => 20)
    p person               # #<OpenStruct name="John", age=20>
    p person.name          # "John"
    p person.adress        # nil
    

    【讨论】:

    • 警告:OpenStruct 实例实际上将允许分配任何属性:person.feather_color = :blue 将适用于您的示例。 Struct 不允许这样做。
    • 感谢您的建议;我看了一下 OpenStruct,似乎介于 Struct 和哈希之间。过去我使用过散列,但我想尝试 Structs 的主要原因是它们严格定义其成员,就像基于散列和 OpenStruct 的数据一样,它们是任意的,如果数据结构很难维护没有很好的记录。使用 Structs 很容易判断其中应该包含什么(尽管什么可以和不可以是 nil 完全是另一回事)。
    • 我建议仅将OpenStruct 用于配置或类似的,但不用于常规开发。为任何丢失的消息获取nil 会伤害你
    【解决方案6】:

    您可以重新排列ifs。

    class MyStruct < Struct
      # Override the initialize to handle hashes of named parameters
      def initialize *args
        # I think this is called a guard clause
        # I suspect the *args is redundant but I'm not certain
        return super *args unless (args.length == 1 and args.first.instance_of? Hash)
        args.first.each_pair do |k, v|
          # I can't remember what having the conditional on the same line is called
          self[k] = v if members.include? k
        end
      end
    end
    

    【讨论】:

    • 您的代码看起来更像 Ruby 并且更干净。我肯定会采用这种风格;谢谢!
    【解决方案7】:

    基于@Andrew Grimm 的回答,但使用 Ruby 2.0 的关键字参数:

    class Struct
    
      # allow keyword arguments for Structs
      def initialize(*args, **kwargs)
        param_hash = kwargs.any? ? kwargs : Hash[ members.zip(args) ]
        param_hash.each { |k,v| self[k] = v }
      end
    
    end
    

    请注意,这不允许混合常规参数和关键字参数 - 您只能使用其中一个。

    【讨论】:

    • 调用super(*args) 允许混合位置参数和关键字参数,并大大简化剩余的行。
    【解决方案8】:

    如果你的哈希键是有序的,你可以调用 splat 操作符来救援:

    NavLink = Struct.new(:name, :url, :title)
    link = { 
      name: 'Stack Overflow', 
      url: 'https://stackoverflow.com', 
      title: 'Sure whatever' 
    }
    actual_link = NavLink.new(*link.values) 
    #<struct NavLink name="Stack Overflow", url="https://stackoverflow.com", title="Sure whatever"> 
    

    【讨论】:

      【解决方案9】:

      如果您确实需要混合常规参数和关键字参数,您始终可以手动构造初始化器...

      Movie = Struct.new(:title, :length, :rating) do
        def initialize(title, length: 0, rating: 'PG13')
          self.title = title
          self.length = length
          self.rating = rating
        end
      end
      
      m = Movie.new('Star Wars', length: 'too long')
      => #<struct Movie title="Star Wars", length="too long", rating="PG13">
      

      这有标题作为强制性的第一个参数只是为了说明。它还有一个优点是您可以为每个关键字参数设置默认值(尽管在处理电影时这不太可能有帮助!)。

      【讨论】:

        【解决方案10】:

        对于 Struct 行为的 1 对 1 等价物(未给出所需参数时引发),我有时会使用它(Ruby 2+):

        def Struct.keyed(*attribute_names)
          Struct.new(*attribute_names) do
            def initialize(**kwargs)
              attr_values = attribute_names.map{|a| kwargs.fetch(a) }
              super(*attr_values)
            end
          end
        end
        

        从那里开始

        class SimpleExecutor < Struct.keyed :foo, :bar
          ...
        end
        

        如果你错过了一个参数,这将引发KeyError,这对于更严格的构造函数和具有大量参数、数据传输对象等的构造函数来说非常好。

        【讨论】:

        • 如果您希望所有参数都是必需的,这很好。避免不必要继承的两项改进:1) 不需要Class.new;只需使用 Struct.new(...) do ... end ; 2)只分配而不是继承:SimpleExecutor = Struct.keyed ...
        • 啊,所以 Struct.new 允许定义方法并将您放入构造函数主体!非常酷@Kelvin 不知道,谢谢
        【解决方案11】:

        这并不能完全回答这个问题,但我发现如果你说出你希望结构化的值的哈希值,它会很好地工作。它的好处是无需记住属性的顺序,同时也不需要对 Struct 进行子类化。

        MyStruct = Struct.new(:height, :width, :length)

        hash = {height: 10, width: 111, length: 20}

        MyStruct.new(*MyStruct.members.map {|key| hash[key] })

        【讨论】:

          【解决方案12】:

          仅限 Ruby 2.x(如果您需要关键字参数,则为 2.1)。仅在 MRI 中测试。

          def Struct.new_with_kwargs(lamb)
            members = lamb.parameters.map(&:last)
            Struct.new(*members) do
              define_method(:initialize) do |*args|
                super(* lamb.(*args))
              end
            end
          end
          
          Foo = Struct.new_with_kwargs(
            ->(a, b=1, *splat, c:, d: 2, **kwargs) do
              # must return an array with values in the same order as lambda args
              [a, b, splat, c, d, kwargs]
            end
          )
          

          用法:

          > Foo.new(-1, 3, 4, c: 5, other: 'foo')
          => #<struct Foo a=-1, b=3, splat=[4], c=5, d=2, kwargs={:other=>"foo"}>
          

          次要的缺点是您必须确保 lambda 以正确的顺序返回值;最大的好处是您拥有 ruby​​ 2 的关键字 args 的全部功能。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-02-24
            • 2012-03-25
            • 2011-01-03
            • 2010-11-11
            • 2021-08-21
            • 2018-07-11
            • 1970-01-01
            相关资源
            最近更新 更多