Ruby Structs中的命名参数

时间:2011-03-23 15:48:32

标签: ruby struct named-parameters

我是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中的Structs将其成员存储为符号,但在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风格。

12 个答案:

答案 0 :(得分:19)

综合现有答案揭示了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 +所需的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">

答案 1 :(得分:12)

你知道的越多越好。无需知道底层数据结构是使用符号还是字符串,甚至是否可以将其作为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">

答案 2 :(得分:9)

允许Ruby关键字参数(Ruby&gt; = 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(一个OpenStruct)。

答案 3 :(得分: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

答案 4 :(得分:4)

您可以重新排列if s。

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

答案 5 :(得分:2)

基于@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

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

答案 6 :(得分:1)

如果你确实需要混合常规和关键字参数,你总是可以手工构建初始化程序......

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">

这个标题是强制性的第一个参数,仅用于说明。它还具有以下优点:您可以为每个关键字参数设置默认值(尽管在处理电影时这不太有用!)。

答案 7 :(得分:1)

对于具有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,对于更严格的构造函数和带有大量参数,数据传输对象等的构造函数来说真的很好。

答案 8 :(得分:1)

如果您的哈希键按顺序排列,您可以调用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 :(得分:0)

这并没有完全回答这个问题,但我发现如果你想说出你希望结构化的价值观,那就可以了。它的好处是可以减少记住属性顺序的需要,同时也不需要subClass Struct。

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

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

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

答案 10 :(得分:0)

仅限Ruby 2.x(如果您需要必需的关键字args,则为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的全部功能。

答案 11 :(得分:0)

使用较新版本的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">