33

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

4

12 回答 12

20

综合现有答案揭示了 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">
于 2016-08-07T05:19:56.930 回答
15

使用较新版本的 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">
于 2019-07-22T14:43:18.670 回答
13

你知道的越少越好。无需知道底层数据结构是使用符号还是字符串,甚至是否可以作为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">
于 2013-06-12T09:25:22.980 回答
9

允许 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(a la OpenStruct)。

于 2015-03-17T21:55:13.400 回答
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
于 2011-03-23T16:38:35.590 回答
4

您可以重新排列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
于 2011-03-23T22:48:27.613 回答
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

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

于 2013-03-25T21:04:52.947 回答
2

如果你的哈希键是有序的,你可以调用 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"> 
于 2016-02-09T14:27:38.207 回答
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">

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

于 2014-05-29T09:29:03.663 回答
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

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

于 2015-04-25T09:25:46.450 回答
0

仅限 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 的全部功能。

于 2016-02-04T23:14:44.017 回答
0

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

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

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

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

于 2015-11-03T18:18:29.270 回答