48

A Ruby Struct allows an instance to be generated with a set of accessors:

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

This looks convenient and powerful, however, a Hash does something pretty similar:

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

What are the real-world situations where I should prefer a Struct (and why), and what are the caveats or pitfalls in choosing one over the other?

6
  • I would consider a struct easier to understand, that is, that it leads to more maintainable code. I will leave it to someone else to comment on any performance advantages. Commented May 1, 2009 at 5:27
  • Why not specify a customer class in such a scenario? Convenience? Commented May 1, 2009 at 7:39
  • 1
    Using Customer = Struct.new does define a Customer class, just with certain default behaviour. You can easily modify or override this behaviour if you wish. Commented May 1, 2009 at 7:59
  • 1
    Also worth noting that Structs outperform Hashes in terms of data retrieval speed, which make them better choices for any configuration that need to be accessed repeatedly at runtime. Commented Jun 17, 2012 at 18:49
  • 2
    See the section "Struct vs. OpenStruct vs. Hash" in Structs inside out. Commented Jan 23, 2013 at 10:40

6 Answers 6

22

Personally I use a struct in cases when I want to make a piece of data act like a collection of data instead of loosely coupled under a Hash.

For instance I've made a script that downloads videos from Youtube and in there I've a struct to represent a Video and to test whether all data is in 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 

Later on in my code I've a loop that goes through all rows in the videos source HTML-page until empty? doesn't return true.

Another example I've seen is James Edward Gray IIs configuration class which uses OpenStruct to easily add configuration variables loaded from an external file:

#!/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 

The difference between Struct and OpenStruct is that Struct only responds to the attributes that you've set, OpenStruct responds to any attribute set - but those with no value set will return Nil

Sign up to request clarification or add additional context in comments.

5 Comments

Why doesn't all the code I wrote get shown here but if I go to edit the post I see it all and it looks great in the preview?
The << screwed it up, turned it into &lt;&lt; and now all is great. :)
Thanks, these are good examples. The first one seems to clearly demonstrate the advantages of Struct. The second one, while a nice example, would work just as well as a Hash wouldn't it?
Yeah the second example could also just be a Hash, personally I just like the syntax better for a configuration file like that. Plus when writing programs where programmers aren't the ones configuring I think config.value = 'something' is a bit clearer than: config[:value] => 'something'. A matter of taste :)
just to chime in, the "Config" class can be also be used for the context object design pattern
13

It's mainly performance. Struct is much faster, by order of magnitudes. And consumes less memory when compared to Hash or OpenStruct. More info here: When should I use Struct vs. OpenStruct?

3 Comments

This answer should be upvoted and it's link is the much deeper answer here. Performance is an order of magnitude or more better.
Struct is faster? Be careful using old information.
Sorry what? Struct comared to Hash is substiantly faster by order of magnitude on Ruby 2.0 and still 10x faster on Ruby 2.4 on my laptop.
12

A Struct has the feature that you can get at its elements by index as well as by name:

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

which sometimes is useful.

Comments

11

Here is more readable benchmarks for those who loves IPS(iteration per seconds) metrics:

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

results:

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 

For huge list:

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 

results:

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 

Conclusion

As you can see struct is a little bit faster, but it requires to define struct fields before using it, so if performance is really matter for you use struct ;)

1 Comment

two small notes on the performance: (i) hash initializes faster, struct has faster access – the more you read, the greater the advantage of struct. (ii) struct initialization is several times slower with keyword arguments, e.g. MyStruct.new(a: 1)
6

Regarding comments about the speed of using Hashes, Struct or OpenStruct: Hash will always win for general use. It's the basis of OpenStruct without the additional icing so it's not as flexible, but it's lean and mean.

Using 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 

Using more concise definitions of the hash and 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 

If the structure, Hash or Struct or OpenStruct is defined once then used many times, then the access speed becomes more important, and a Struct begins to shine:

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 

Notice though, that the Struct is only 4x faster than the Hash for accessing, whereas the Hash is 17x faster for initialization, assignment and accessing. You'll have to figure out which is the best to use based on the needs of a particular application. I tend to use Hashes for general use as a result.

Also, the speed of using OpenStruct has improved greatly over the years; It used to be slower than Struct based on benchmarks I've seen in the past and comparing to 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 

and:

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 

and:

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 

2 Comments

This is an edge case (one accessor) and you are comaring mix of initialization, get and set. What's generally most important is access speed. But nice observation about improvement in time.
I'm comparing both overall speed, when initialization is included in the test, and access speed only, since there are differences, and different uses might emphasize one over the other.
2

I have rerun @mpospelov's benchmarks on Ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin20].

Simple

Warming up -------------------------------------- hash 1.008M i/100ms struct 423.906k i/100ms ostruct 16.384k i/100ms Calculating ------------------------------------- hash 9.923M (± 1.9%) i/s - 50.412M in 5.082029s struct 4.327M (± 2.6%) i/s - 22.043M in 5.097358s ostruct 158.771k (± 6.7%) i/s - 802.816k in 5.084066s Comparison: hash: 9923144.6 i/s struct: 4327316.1 i/s - 2.29x (± 0.00) slower ostruct: 158771.4 i/s - 62.50x (± 0.00) slower 

Huge List

Warming up -------------------------------------- hash 71.378k i/100ms struct 99.245k i/100ms ostruct 855.000 i/100ms Calculating ------------------------------------- hash 712.113k (± 4.9%) i/s - 3.569M in 5.024094s struct 1.098M (± 2.9%) i/s - 5.558M in 5.066160s ostruct 8.629k (± 4.8%) i/s - 43.605k in 5.066147s Comparison: struct: 1098071.6 i/s hash: 712112.5 i/s - 1.54x (± 0.00) slower ostruct: 8628.8 i/s - 127.26x (± 0.00) slower 

Conclusion

  • If performance does matter, use struct.
  • If defining fields on the fly matters, go with open struct.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.