Quand est-il préférable d’utiliser un Struct plutôt qu’un Hash dans Ruby?

Un Ruby Struct permet à une instance d’être générée avec un ensemble d’accesseurs:

# Create a structure named by its constant Customer = Struct.new(:name, :address) #=> Customer Customer.new("Dave", "123 Main") #=> # 

Cela semble pratique et puissant, cependant, un Hash fait quelque chose de très similaire:

 Customer = {:name => "Dave", :address => "123 Main"} 

Quelles sont les situations réelles dans lesquelles je devrais préférer une structure (et pourquoi), et quelles sont les mises en garde ou les pièges dans le choix de l’une ou de l’autre?

Personnellement, j’utilise une structure dans les cas où je veux faire en sorte qu’un morceau de données agisse comme une collection de données au lieu d’être couplé de manière souple sous un Hash .

Par exemple, j’ai créé un script qui télécharge des vidéos de Youtube et j’ai ici une structure pour représenter une vidéo et tester si toutes les données sont en place:

 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 

Plus tard, dans mon code, une boucle traverse toutes les lignes de la page HTML de la source vidéo jusqu’à ce qu’elle soit empty? ne retourne pas vrai.

Un autre exemple que j’ai vu est la classe de configuration James Edward Gray IIs qui utilise OpenStruct pour append facilement des variables de configuration chargées à partir d’un fichier externe:

 #!/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 différence entre Struct et OpenStruct fait que Struct ne répond qu’aux atsortingbuts que vous avez définis, que OpenStruct répond à tout ensemble d’atsortingbuts, mais que ceux sans valeur définie renvoient Nil

Une structure a la fonctionnalité que vous pouvez obtenir à ses éléments par index aussi bien que par nom:

 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 

ce qui est parfois utile.

C’est principalement la performance. Struct est beaucoup plus rapide, par ordre de grandeur. Et consum moins de mémoire que Hash ou OpenStruct. Plus d’infos ici: Quand dois-je utiliser Struct vs OpenStruct?

En ce qui concerne les commentaires sur la rapidité d’utilisation de Hashes, Struct ou OpenStruct: Hash sera toujours gagnant pour un usage général. C’est la base d’OpenStruct sans le givrage supplémentaire, donc ce n’est pas aussi flexible, mais c’est maigre et méchant.

Utiliser 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 

En utilisant des définitions plus concises du hachage et de 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 structure, Hash ou Struct ou OpenStruct est définie une fois puis utilisée à plusieurs resockets, la vitesse d’access devient plus importante et une structure commence à briller:

 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 

Notez cependant que le Struct n’est que 4 fois plus rapide que le Hash pour y accéder, alors que le Hash est 17 fois plus rapide pour l’initialisation, l’affectation et l’access. Vous devrez déterminer quel est le meilleur à utiliser en fonction des besoins d’une application particulière. J’ai tendance à utiliser Hashes pour un usage général en conséquence.

En outre, la vitesse d’utilisation d’OpenStruct s’est considérablement améliorée au fil des ans. Auparavant, il était plus lent que Struct sur la base de tests comparatifs que j’ai vus dans le passé et comparés à 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 

et:

 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 

et:

 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 

Voici des repères plus lisibles pour ceux qui aiment les mesures IPS (itération par seconde):

Pour les petites instances:

 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); aa } x.report('ostruct') { a = OpenStruct.new(a: 1); aa } x.compare! end 

résultats:

 Warming up -------------------------------------- hash 147.162ki/100ms struct 171.949ki/100ms ostruct 21.086ki/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 

Pour liste énorme:

 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 

résultats:

 Warming up -------------------------------------- hash 51.741ki/100ms struct 62.346ki/100ms ostruct 1.010ki/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 

Conclusion

Comme vous pouvez le voir, struct est un peu plus rapide, mais il faut définir des champs de structure avant de l’utiliser, donc si les performances sont vraiment importantes pour vous, utilisez struct;)