Именованные параметры в Ruby Structs

Я новичок в 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 и обнаружил, что Structs в 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, которые я пробовал.

11 ответов

Чем меньше вы знаете, тем лучше. Не нужно знать, используют ли лежащую в основе структуру данных символы или строку или даже могут ли они быть адресованы как 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"
 => #


Синтез существующих ответов показывает гораздо более простой вариант для 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"> 
</struct></struct>

Если вы хотите, чтобы аргументы, подобные 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") # => #


Решение, которое разрешает только аргументы ключевого слова 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).


Вы рассматривали 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>


Вы можете изменить порядок if.

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


Основываясь на ответе @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

Обратите внимание, что это не позволяет смешивать регулярные аргументы и ключевые слова - вы можете использовать только один или другой.


Если вам нужно смешать регулярные аргументы и ключевые слова, вы всегда можете создать инициализатор вручную...

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')
=> #

Этот заголовок является обязательным первым аргументом для иллюстрации. Это также имеет то преимущество, что вы можете установить значения по умолчанию для каждого аргумента ключевого слова (хотя это вряд ли будет полезно при работе с Movies!).


Для эквивалента 1-к-1 с поведением Struct (повышение, когда требуемый аргумент не задан) я иногда использую это (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, если вы пропустили аргумент, настолько реальным для более строгих конструкторов и конструкторов с большим количеством аргументов, объектов передачи данных и т.п.


это точно не отвечает на вопрос, но я нашел, что он работает хорошо, если вы говорите хэш значений, которые хотите структурировать. Он имеет преимущество выгрузки необходимости запоминать порядок атрибутов, а также не нужен для subClass Struct.

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

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

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


Ruby 2.x only (2.1 если вы хотите, чтобы требуемые ключевые слова args). Проверено только на МРТ.

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"}>
</struct>

Недостатком является то, что вы должны убедиться, что лямбда возвращает значения в правильном порядке; большой потенциал заключается в том, что у вас есть полная мощность рубинового ключевого слова args.


Если ваши хеш-ключи в порядке, вы можете вызвать оператора 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"> 
</struct>

licensed under cc by-sa 3.0 with attribution.