p.matsinopoulos
p.matsinopoulos

Reputation: 7810

Why does Rails Active Support deep_dup not make a copy of the array embedded into the object being cloned?

I have an object that embeds an Array in its state. I am using deep_dup on the object instance but the returned object references the same array object as the original instance, which is not what I was expecting.

Here is what I do:

class A
  def initialize(arr)
    @arr = arr.deep_dup
  end

  attr_accessor :arr
end

a = A.new(['a', 'b', 'c'])
# => #<A:0x007fe6eb7ae458 @arr=["a", "b", "c"]> 
b = a.deep_dup
# => #<A:0x007fe6f382ad20 @arr=["a", "b", "c"]> 
b.arr[1] = 'e'
# => "e" 
a
# => #<A:0x007fe6eb7ae458 @arr=["a", "e", "c"]> 
b
# => #<A:0x007fe6f382ad20 @arr=["a", "e", "c"]> 
a.arr.object_id
# => 70314884952600 
b.arr.object_id
# => 70314884952600 

I have read in the documentation of deep_dup, that

The deep_dup method returns a deep copy of a given object.

What am I doing or understanding wrong?

Upvotes: 2

Views: 2386

Answers (1)

Simple Lime
Simple Lime

Reputation: 11035

If you take a look at the source for deep_dup, on Object

def deep_dup
  duplicable? ? dup : self
end

it just calls dup (duplicable? is always true, by default). Where, for arrays it calls deep_dup on every element:

def deep_dup
  map(&:deep_dup)
end

Looking at dup for Object (since that's what ends up being called on A):

Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference. dup copies the tainted state of obj.

So, to get your example to work, you'd need something like:

class A
  attr_accessor :array
  def initialize(array)
    @array = array.deep_dup
  end

  def initialize_copy(copy)
    copy.array = self.array.deep_dup

    super
  end
end

a = A.new(['a', 'b', 'c'])
b = a.deep_dup
b.array[1] = 'e'

puts a.inspect          # => #<A:0x007f857be2f510 @array=["a", "b", "c"]>
puts b.inspect          # => #<A:0x007f857be2f448 @array=["a", "e", "c"]>
puts a.array.object_id  # => 70105642924 560 (spaces added for clarity)
puts b.array.object_id  # => 70105642924 660

The initialize_copy method isn't well documented anywhere, but the dup method states

This method may have class-specific behavior. If so, that behavior will be documented under the #initialize_copy method of the class.

So it's set up as a way to customize the dup process without actually overriding dup and this documentation (which is the only real documentation I can find on that method) shows that it only takes the 1 parameter, which is the copy

Upvotes: 3

Related Questions