into - ¿Cuándo es mejor usar un Struct en lugar de un Hash en Ruby?
ruby puts hash (5)
Un Ruby Struct permite generar una instancia con un conjunto de accesores:
# Create a structure named by its constant
Customer = Struct.new(:name, :address) #=> Customer
Customer.new("Dave", "123 Main") #=> #<Customer name="Dave", address="123 Main">
Esto parece conveniente y poderoso, sin embargo, un Hash hace algo bastante similar:
Customer = {:name => "Dave", :address => "123 Main"}
¿Cuáles son las situaciones del mundo real en las que debería preferir un Struct (y por qué), y cuáles son las advertencias o inconvenientes al elegir uno sobre el otro?
A Struct tiene la característica que puede obtener en sus elementos por índice y por nombre:
irb(main):004:0> Person = Struct.new(:name, :age)
=> Person
irb(main):005:0> p = Person.new("fred", 26)
=> #
irb(main):006:0> p[0]
=> "fred"
irb(main):007:0> p[1]
=> 26
irb(main):008:0> p.name
=> "fred"
irb(main):009:0> p.age
=> 26
lo que a veces es útil.
Aquí hay puntos de referencia más legibles para aquellos que aman las métricas de IPS (iteración por segundo):
Para pequeños casos:
require ''benchmark/ips''
require ''ostruct''
MyStruct = Struct.new(:a)
Benchmark.ips do |x|
x.report(''hash'') { a = { a: 1 }; a[:a] }
x.report(''struct'') { a = MyStuct.new(1); a.a }
x.report(''ostruct'') { a = OpenStruct.new(a: 1); a.a }
x.compare!
end
resultados:
Warming up --------------------------------------
hash 147.162k i/100ms
struct 171.949k i/100ms
ostruct 21.086k i/100ms
Calculating -------------------------------------
hash 2.608M (± 3.1%) i/s - 13.097M in 5.028022s
struct 3.680M (± 1.8%) i/s - 18.399M in 5.001510s
ostruct 239.108k (± 5.5%) i/s - 1.202M in 5.046817s
Comparison:
struct: 3679772.2 i/s
hash: 2607565.1 i/s - 1.41x slower
ostruct: 239108.4 i/s - 15.39x slower
Para la lista enorme:
require ''benchmark/ips''
require ''ostruct''
MyStruct = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z)
Benchmark.ips do |x|
x.report(''hash'') do
hash = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 }
hash[:a]; hash[:b]; hash[:c]; hash[:d]; hash[:e]; hash[:f]; hash[:g]; hash[:h]; hash[:i]; hash[:j]; hash[:k]; hash[:l]; hash[:m]; hash[:n]; hash[:o]; hash[:p]; hash[:q]; hash[:r]; hash[:s]; hash[:t]; hash[:u]; hash[:v]; hash[:w]; hash[:x]; hash[:y]; hash[:z]
end
x.report(''struct'') do
struct = MyStruct.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26)
struct.a;struct.b;struct.c;struct.d;struct.e;struct.f;struct.g;struct.h;struct.i;struct.j;struct.k;struct.l;struct.m;struct.n;struct.o;struct.p;struct.q;struct.r;struct.s;struct.t;struct.u;struct.v;struct.w;struct.x;struct.y;struct.z
end
x.report(''ostruct'') do
ostruct = OpenStruct.new( a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26)
ostruct.a;ostruct.b;ostruct.c;ostruct.d;ostruct.e;ostruct.f;ostruct.g;ostruct.h;ostruct.i;ostruct.j;ostruct.k;ostruct.l;ostruct.m;ostruct.n;ostruct.o;ostruct.p;ostruct.q;ostruct.r;ostruct.s;ostruct.t;ostruct.u;ostruct.v;ostruct.w;ostruct.x;ostruct.y;ostruct.z;
end
x.compare!
end
resultados:
Warming up --------------------------------------
hash 51.741k i/100ms
struct 62.346k i/100ms
ostruct 1.010k i/100ms
Calculating -------------------------------------
hash 603.104k (± 3.9%) i/s - 3.053M in 5.070565s
struct 780.005k (± 3.4%) i/s - 3.928M in 5.041571s
ostruct 11.321k (± 3.4%) i/s - 56.560k in 5.001660s
Comparison:
struct: 780004.8 i/s
hash: 603103.8 i/s - 1.29x slower
ostruct: 11321.2 i/s - 68.90x slower
Conclusión
Como puede ver, la estructura es un poco más rápida, pero requiere definir los campos de la estructura antes de usarla, por lo que si el rendimiento es realmente importante para usted, use la estructura;)
Con respecto a los comentarios sobre la velocidad de uso de Hashes, Struct o OpenStruct: Hash siempre ganará para uso general. Es la base de OpenStruct sin la formación de hielo adicional, por lo que no es tan flexible, pero es delgado y mezquino.
Usando Ruby 2.4.1:
require ''fruity''
require ''ostruct''
def _hash
h = {}
h[''a''] = 1
h[''a'']
end
def _struct
s = Struct.new(:a)
foo = s.new(1)
foo.a
end
def _ostruct
person = OpenStruct.new
person.a = 1
person.a
end
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than an_ostruct by 13x ± 1.0
# >> an_ostruct is similar to a_struct
Usando definiciones más concisas del hash y OpenStruct:
require ''fruity''
require ''ostruct''
def _hash
h = {''a'' => 1}
h[''a'']
end
def _struct
s = Struct.new(:a)
foo = s.new(1)
foo.a
end
def _ostruct
person = OpenStruct.new(''a'' => 1)
person.a
end
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than an_ostruct by 17x ± 10.0
# >> an_ostruct is similar to a_struct
Si la estructura, Hash o Struct o OpenStruct se define una vez y luego se usa muchas veces, entonces la velocidad de acceso se vuelve más importante y un Struct comienza a brillar:
require ''fruity''
require ''ostruct''
HSH = {''a'' => 1}
def _hash
HSH[''a'']
end
STRCT = Struct.new(:a).new(1)
def _struct
STRCT.a
end
OSTRCT = OpenStruct.new(''a'' => 1)
def _ostruct
OSTRCT.a
end
puts "Ruby version: #{RUBY_VERSION}"
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Ruby version: 2.4.1
# >> Running each test 65536 times. Test will take about 2 seconds.
# >> a_struct is faster than a_hash by 4x ± 1.0
# >> a_hash is similar to an_ostruct
Sin embargo, tenga en cuenta que el Struct es solo 4 veces más rápido que el Hash para acceder, mientras que el Hash es 17 veces más rápido para la inicialización, asignación y acceso. Tendrá que averiguar cuál es el mejor para usar según las necesidades de una aplicación en particular. Tiendo a usar Hashes para uso general como resultado.
Además, la velocidad de uso de OpenStruct ha mejorado mucho con los años; Solía ser más lento que Struct según los puntos de referencia que he visto en el pasado y en comparación con 1.9.3-p551:
require ''fruity''
require ''ostruct''
def _hash
h = {}
h[''a''] = 1
h[''a'']
end
def _struct
s = Struct.new(:a)
foo = s.new(1)
foo.a
end
def _ostruct
person = OpenStruct.new
person.a = 1
person.a
end
puts "Ruby version: #{RUBY_VERSION}"
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Ruby version: 1.9.3
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than a_struct by 7x ± 1.0
# >> a_struct is faster than an_ostruct by 2x ± 0.1
y:
require ''fruity''
require ''ostruct''
def _hash
h = {''a'' => 1}
h[''a'']
end
def _struct
s = Struct.new(:a)
foo = s.new(1)
foo.a
end
def _ostruct
person = OpenStruct.new(''a'' => 1)
person.a
end
puts "Ruby version: #{RUBY_VERSION}"
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Ruby version: 1.9.3
# >> Running each test 4096 times. Test will take about 2 seconds.
# >> a_hash is faster than a_struct by 7x ± 1.0
# >> a_struct is faster than an_ostruct by 2x ± 1.0
y:
require ''fruity''
require ''ostruct''
HSH = {''a'' => 1}
def _hash
HSH[''a'']
end
STRCT = Struct.new(:a).new(1)
def _struct
STRCT.a
end
OSTRCT = OpenStruct.new(''a'' => 1)
def _ostruct
OSTRCT.a
end
puts "Ruby version: #{RUBY_VERSION}"
compare do
a_hash { _hash }
a_struct { _struct }
an_ostruct { _ostruct }
end
# >> Ruby version: 1.9.3
# >> Running each test 32768 times. Test will take about 1 second.
# >> a_struct is faster than an_ostruct by 3x ± 1.0
# >> an_ostruct is similar to a_hash
Es principalmente el rendimiento. Struct es mucho más rápido, por orden de magnitudes. Y consume menos memoria en comparación con Hash o OpenStruct. Más información aquí: ¿ Cuándo debo usar Struct vs. OpenStruct?
Personalmente utilizo una estructura en los casos en los que quiero que una parte de los datos actúe como una recopilación de datos en lugar de acoplarlos de forma flexible bajo un Hash
.
Por ejemplo, he creado un script que descarga videos de Youtube y allí tengo una estructura para representar un video y para comprobar si todos los datos están en su lugar:
Video = Struct.new(:title, :video_id, :id) do
def to_s
"http://youtube.com/get_video.php?t=#{id}&video_id=#{video_id}&fmt=18"
end
def empty?
@title.nil? and @video_id.nil? and @id.nil?
end
end
Más adelante, en mi código, tengo un bucle que recorre todas las filas de la página HTML de la fuente de videos hasta que empty?
no devuelve verdad
Otro ejemplo que he visto es la clase de configuración de James Edward Gray II , que utiliza OpenStruct
para agregar fácilmente las variables de configuración cargadas desde un archivo externo:
#!/usr/bin/env ruby -wKU
require "ostruct"
module Config
module_function
def load_config_file(path)
eval <<-END_CONFIG
config = OpenStruct.new
#{File.read(path)}
config
END_CONFIG
end
end
# configuration_file.rb
config.db = File.join(ENV[''HOME''], ''.cool-program.db'')
config.user = ENV[''USER'']
# Usage:
Config = Config.load_config(''configuration_file.rb'')
Config.db # => /home/ba/.cool-program.db
Config.user # => ba
Config.non_existant # => Nil
La diferencia entre Struct
y OpenStruct
es que Struct
solo responde a los atributos que ha establecido, OpenStruct
responde a cualquier conjunto de atributos, pero aquellos sin un conjunto de valores devolverán Nil