Именованные параметры в 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, которые я пробовал.
Ответы
Ответ 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+, требовали 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">
Ответ 2
Чем меньше вы знаете, тем лучше. Не нужно знать, используют ли лежащую в основе структуру данных символы или строку или даже могут ли они быть адресованы как 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">
Ответ 3
Решение, которое разрешает только аргументы ключевого слова 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).
Ответ 4
Вы рассматривали 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
Ответ 5
Вы можете изменить порядок 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
Ответ 6
Основываясь на ответе @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
Обратите внимание, что это не позволяет смешивать регулярные аргументы и ключевые слова - вы можете использовать только один или другой.
Ответ 7
Если ваши хеш-ключи в порядке, вы можете вызвать оператора 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">
Ответ 8
Если вам нужно смешать регулярные аргументы и ключевые слова, вы всегда можете создать инициализатор вручную...
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">
Этот заголовок является обязательным первым аргументом для иллюстрации. Это также имеет то преимущество, что вы можете установить значения по умолчанию для каждого аргумента ключевого слова (хотя это вряд ли будет полезно при работе с Movies!).
Ответ 9
Для эквивалента 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
, если вы пропустили аргумент, настолько реальным для более строгих конструкторов и конструкторов с большим количеством аргументов, объектов передачи данных и т.п.
Ответ 10
это точно не отвечает на вопрос, но я нашел, что он работает хорошо, если вы говорите хэш значений, которые хотите структурировать. Он имеет преимущество выгрузки необходимости запоминать порядок атрибутов, а также не нужен для subClass Struct.
MyStruct = Struct.new(:height, :width, :length)
hash = {height: 10, width: 111, length: 20}
MyStruct.new(*MyStruct.members.map {|key| hash[key] })
Ответ 11
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"}>
Недостатком является то, что вы должны убедиться, что лямбда возвращает значения в правильном порядке; большой потенциал заключается в том, что у вас есть полная мощность рубинового ключевого слова args.
Ответ 12
В новых версиях 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">