Подтвердить что ты не робот

Именованные параметры в 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, которые я пробовал.

4b9b3361

Ответ 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">