2011-03-23 12 views
27

Soy bastante nuevo para Ruby así que me disculpo si esta es una pregunta obvia.Parámetros con nombre en Ruby Structs

Me gustaría utilizar parámetros con nombre al crear una instancia de Struct, es decir, ser capaz de especificar qué elementos de Struct obtienen qué valores y el resto es nulo.

Por ejemplo, yo quiero hacer:

Movie = Struct.new :title, :length, :rating 
m = Movie.new :title => 'Some Movie', :rating => 'R' 

Esto no funciona.

Así que se le ocurrió la siguiente:

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' 

Esto parece funcionar bien, pero no estoy seguro de si hay una mejor manera de hacer esto, o si estoy haciendo algo bastante loco . Si alguien puede validar/desmantelar este enfoque, estaría muy agradecido.

ACTUALIZACIÓN

me encontré con este principio en 1.9.2 y funciona bien; sin embargo, habiendo probado en otras versiones de Ruby (agradecerá RVM), funciona/no funciona de la siguiente manera:

  • 1.8.7: No funciona
  • 1.9.1: Trabajo
  • 1.9. 2: Trabajar
  • JRuby (configurado para ejecutarse como 1.9.2): no funciona

JRuby es un problema para mí, ya que me gusta que sea compatible con la que, a efectos de despliegue.

otra actualización

En esta pregunta cada vez mayor laberíntica, he experimentado con las diferentes versiones de Ruby y descubrieron que en la tienda Las estructuras 1.9.x sus miembros como símbolos, pero en 1.8.7 y JRuby , que se almacenan como cadenas, así que actualizan el código a ser las siguientes (teniendo en las indicaciones ya dadas amablemente):

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' 

Esto ahora parece funcionar para todos los sabores de Ruby que he probado.

+0

Tu código se ve muy bien, de verdad. –

+0

He estado buscando lo mismo: ¿hay una gema estándar que hace esto, junto con especificar qué argumentos son necesarios? –

+0

https://github.com/chemica/solid-struct hace esto, pero no aplica los argumentos requeridos. –

Respuesta

5

¿Has considerado 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

Advertencia: la instancia 'OpenStruct' permitirá prácticamente cualquier atributo asignado:' person.feather_color =: blue' funcionará con tu ejemplo. 'Struct' no permite esto. –

+0

Gracias por la sugerencia; Eché un vistazo a OpenStruct y parecía estar en algún lugar entre un Struct y un hash. En el pasado he usado hash, pero la razón principal por la que quiero probar Structs es que definen estrictamente a sus miembros, ya que con los datos basados ​​en hashes y OpenStruct son arbitrarios y puede ser difícil de mantener si la estructura de datos no está bien documentado Con Structs es fácil decir qué debería haber allí (aunque lo que puede y no puede ser nulo es otra cosa). –

+0

Sugiero solo usar 'OpenStruct' para config o similar pero NO para desarrollo regular. Obtener 'nil' por CUALQUIER mensaje que falte te lastimará – ecoologic

4

Puede reorganizar el 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 
+0

Tu código parece mucho más Ruby y limpio . Definitivamente adoptaré ese estilo; ¡gracias por eso! –

2

Sobre la base de la respuesta de @ Andrew Grimm, pero utilizando Ruby 2.0 argumentos de palabra clave:

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 

en cuenta que esta no permite la mezcla de regular y palabra clave arguments-- sólo se puede utilizar uno o el otro.

+0

una llamada a 'super (* args)' permite la mezcla de argumentos posicionales y de palabra clave, y permite una considerable simplificación de las líneas restantes. –

12

Cuanto menos sepa, mejor. No es necesario saber si la estructura de datos subyacente utiliza símbolos o cadena, o incluso si se puede direccionar como Hash.Sólo tiene que utilizar los emisores de atributos:

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 

Se necesita tanto argumentos posicionales y de palabras clave:

> KwStruct.new "q", :zxcv => "z" 
=> #<struct KwStruct qwer="q", asdf=nil, zxcv="z"> 
+1

Ruby wizardry. Gracias, me gusta esto – Subimage

+0

¡Respuesta muy inteligente! Probado y funcionando en Ruby 1.9 y 2.x – ahirschberg

1

Si necesita mezclar argumentos regulares y de palabras clave, siempre se puede construir el inicializador con la mano ...

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

Esto tiene el título como un primer argumento obligatorio solo para ilustración. También tiene la ventaja de que puede establecer los valores predeterminados para cada argumento de palabra clave (¡aunque es poco probable que sea útil si se trata de películas!).

9

Una solución que solamente permite los argumentos de la palabra clave Ruby (Ruby> = 2.0).

class KeywordStruct < Struct 
    def initialize(**kwargs) 
    super(kwargs.keys) 
    kwargs.each { |k, v| self[k] = v } 
    end 
end 

Uso:

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 

Este tipo de estructura puede ser muy útil como un objeto de valor, especialmente si te gusta más estrictos métodos de acceso método que en realidad va de error en lugar de volver nil (al estilo de OpenStruct).

+0

Esta solución falla al dar argumentos cero: 'foo = Foo.new()' obtiene '' struct Foo bar = [], baz = nil, qux = nil> '. Use @ indirect's [answer] (https://stackoverflow.com/a/38811145/165673) en su lugar. – Yarin

1

Para un equivalente de 1-a-1 con el comportamiento Struct (subir cuando no se da el argumento requerido) yo uso esto a veces (Rubí 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 

y de ahí en

class SimpleExecutor < Struct.keyed :foo, :bar 
    ... 
end 

Esto aumentará un KeyError si se perdió un argumento, muy agradable para constructores y constructores más estrictos con muchos argumentos, objetos de transferencia de datos y similares.

+1

Esto es bueno si quieres que se necesiten todos los argumentos. Dos mejoras para evitar herencia innecesaria: 1) 'Class.new' no es necesario; solo use 'Struct.new (...) do ... end'; 2) Simplemente asigne en lugar de heredar: 'SimpleExecutor = Struct.keyed ...' – Kelvin

+0

¡Así que Struct.new permite definiciones de métodos y lo coloca en el cuerpo del constructor! muy bien @ Kelvin no lo sabía, gracias – Julik

0

esto no responde exactamente la pregunta, pero encontré que funciona bien si tiene un hash de valores que desea estructurar. Tiene el beneficio de descargar la necesidad de recordar el orden de los atributos sin necesidad de subClass Struct.

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

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

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

0

Rubí 2.x sólo 2.1 (si se desea obligado argumentos de palabras clave). Solo probado en 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 
) 

Uso:

> Foo.new(-1, 3, 4, c: 5, other: 'foo') 
=> #<struct Foo a=-1, b=3, splat=[4], c=5, d=2, kwargs={:other=>"foo"}> 

El pequeño inconveniente es que hay que garantizar la lambda devuelve los valores en el orden correcto; la gran ventaja es que tienes todo el poder de las args de palabras clave de ruby ​​2.

1

Si sus claves hash están en orden se puede llamar al operador splat al rescate:

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

síntesis de las respuestas existentes revela una opción mucho más simple para Ruby 2.0+:

class KeywordStruct < Struct 
    def initialize(**kwargs) 
    super(*members.map{|k| kwargs[k] }) 
    end 
end 

uso es idéntica a la existente Struct, donde cualquier argumento no se da por defecto a 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"> 

Si desea exigir que los argumentos como Ruby 2.1 + 's kwargs requeridos , que es un cambio muy pequeño:

class RequiredKeywordStruct < Struct 
    def initialize(**kwargs) 
    super(*members.map{|k| kwargs.fetch(k) }) 
    end 
end 

en ese momento, anulando initialize para dar ciertos valores por defecto kwargs también es factible:

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

Interesante. Ejecuté benchmark/ips en su implementación, y es aproximadamente 2 veces más lento que simplemente implementar el objeto Pet, en lugar de una estructura. Obj es aproximadamente 2 veces más rápido. Sin embargo, tu Keyword Struct sigue siendo aproximadamente 3 veces más rápida que Openstruct, por lo que si no necesitas esa flexibilidad, es una opción viable. – konung

Cuestiones relacionadas