When is it better to use a Struct rather than a Hash in Ruby?

前端 未结 6 1230
清歌不尽
清歌不尽 2021-01-30 20:44

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)     #         


        
相关标签:
6条回答
  • 2021-01-30 20:53

    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?

    0 讨论(0)
  • 2021-01-30 20:57

    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.

    0 讨论(0)
  • 2021-01-30 21:03

    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

    0 讨论(0)
  • 2021-01-30 21:05

    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.
    0 讨论(0)
  • 2021-01-30 21:08

    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 ;)

    0 讨论(0)
  • 2021-01-30 21:08

    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
    
    0 讨论(0)
提交回复
热议问题