Changing state of Ruby objects changes other class

2019-05-21 08:56发布

问题:

I have a class that primarily implements some logic around a multi-dimensional array, which is essentially a grid of numbers. These numbers can swap positions, etc.. However, when they swap, other objects of the same class also appear to be modified. I'm not sure why.

I'm using instance variables to store the grid, so I don't understand why changes are apparently affecting other class members.

Here's a simplified example;

class TestGrid
attr_accessor :grid
@grid = []

def initialize(newgrid)
    @grid = newgrid
end

def to_s
    out = ""
    @grid.each{|row|
      out += row.join("|") + "\n"
    }
    out
end

def swap(x, y)
    @grid[x[0]][x[1]], @grid[y[0]][y[1]] = @grid[y[0]][y[1]], @grid[x[0]][x[1]]
end

end

When we interact with a single instance in IRB, things look fine;

1.9.3-p385 :001 > require './TestGrid.rb'
 => true 
1.9.3-p385 :002 > x = TestGrid.new([[1,2],[3,4]])
 => 1|2
 3|4

1.9.3-p385 :003 > x.swap([0,1],[1,1])
 => [4, 2] 
1.9.3-p385 :004 > puts  x
1|4
3|2
 => nil 

However, if I create a second instance by cloning or duping;

1.9.3-p385 :006 >   x = TestGrid.new([[1,2],[3,4]])
 => 1|2
3|4

1.9.3-p385 :007 > y = x.clone
 => 1|2
3|4

1.9.3-p385 :008 > x.swap([0,1],[1,1])
 => [4, 2] 
1.9.3-p385 :009 > puts x 
1|4
3|2
 => nil 
1.9.3-p385 :010 > puts y
1|4
3|2
 => nil 

Why are my changes to x also being applied to y? From my understanding of Object#Clone, theses are supposed to be distinct instance, unrelated to each other. Their object ID's would seem to support that expectation;

1.9.3-p385 :012 > puts "#{x.object_id} #{y.object_id}"
70124426240320 70124426232820

For reference, I ended up creating an initialize_copy method which ensures the affected parameter is deep copied. I didn't really like the idea of Marshalling objects around just to copy an array deeply, so I decided on this instead.

def initialize_copy(original)
  super
  @grid = []
  original.grid.each{|inner|
    @grid << inner.dup
  }
 end

回答1:

By default, dup and clone produce shallow copies of the objects they are invoked on. Meaning that x and y in your example still reference the same area of memory.

http://ruby-doc.org/core-2.0/Object.html#method-i-dup

http://ruby-doc.org/core-2.0/Object.html#method-i-clone

You can override them inside of your customized class to produce a deep copy in a different area of memory.

A common idiom in Ruby is to use the Marshal#load and Marshal#dump methods of the Object superclass to produce deep copies. (Note: these methods are normally used to serialize/deserialze objects).

 def dup
   new_grid = Marshal.load( Marshal.dump(@grid) )

   new_grid
 end

irb(main):007:0> x = TestGrid.new([[1,2],[3,4]])
=> 1|2
3|4

irb(main):008:0> y = x.dup
=> [[1, 2], [3, 4]]
irb(main):009:0> x.swap([0,1],[1,1])
=> [4, 2]
irb(main):010:0> puts x
1|4
3|2
=> nil
irb(main):011:0> y
=> [[1, 2], [3, 4]]
irb(main):012:0> puts y
1
2
3
4
=> nil
irb(main):013:0>

y remains the same after the swap.

Alternatively, create a new array, iterate through @grid and push its subarrays into the array.

 def dup
   new_grid = []

   @grid.each do |g|
      new_grid << g
   end

   new_grid
 end