Search code examples
rubyoopinstantiationinstance-variablesclass-variables

Ruby class variable instance variable bug


Game has an array of ten Frame instances

class Frame
  attr_accessor :rolls
  def initialize
    @rolls = ""
  end
  
end

class Game
  attr_accessor :frames
  def initialize
    @frames = Array.new(10, Frame.new) 
  end
  
  def print_frames
    @frames.each_with_index do |frame, idx|
      p "Frame ##{idx+1}: #{frame.rolls}"
    end
  end
end

game = Game.new

rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2]

curr = 0
rolls.each_with_index do |roll|
  game.frames[curr].rolls << roll.to_s
  if game.frames[curr].rolls.size == 2
    curr += 1
  end
end

p "Total rolls: #{rolls.size}"
p game.print_frames

I expect 10 lines to be printed, each line has a frame id with a string of 2 numbers however, the following is returned

"Total rolls: 20"
"Frame #1: 55374000000000000022"
"Frame #2: 55374000000000000022"
"Frame #3: 55374000000000000022"
"Frame #4: 55374000000000000022"
"Frame #5: 55374000000000000022"
"Frame #6: 55374000000000000022"
"Frame #7: 55374000000000000022"
"Frame #8: 55374000000000000022"
"Frame #9: 55374000000000000022"
"Frame #10: 55374000000000000022"
[#<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">, #<Frame:0x0000565402e40528 @rolls="55374000000000000022">]

instance variable is acting like class variables shared between frames. all 20 rolls numbers are concatenated instead of each frame owning a pair.
what is wrong with the code? is it because the Game or Frame object is not instantiated correctly?


Solution

  • Array.new(10, Frame.new)
    

    Creates an array with 10 elements all pointing to a single Frame instance. To create an array with 10 separate Frame instances you should use the block form.

    Array.new(10) { Frame.new }
    

    This executes the block 10 times and assigns the result of each execution to the cosponsoring index.

    See: Creating Arrays

    An array can also be created by explicitly calling ::new with zero, one (the initial size of the Array) or two arguments (the initial size and a default object).

    ary = Array.new    #=> []
    Array.new(3)       #=> [nil, nil, nil]
    Array.new(3, true) #=> [true, true, true]
    

    Note that the second argument populates the array with references to the same object. Therefore, it is only recommended in cases when you need to instantiate arrays with natively immutable objects such as Symbols, numbers, true or false.

    To create an array with separate objects a block can be passed instead. This method is safe to use with mutable objects such as hashes, strings or other arrays:

    Array.new(4) {Hash.new}    #=> [{}, {}, {}, {}]
    Array.new(4) {|i| i.to_s } #=> ["0", "1", "2", "3"]