11
arr = ["red","green","yellow"]

arr2 = arr.clone
arr2[0].replace("blue")

puts arr.inspect
puts arr2.inspect

produces:

["blue", "green", "yellow"]
["blue", "green", "yellow"]

Is there anyway to do a deep copy of an array of strings, other than using Marshal as i understand that is a hack.

I could do:

arr2 = []
arr.each do |e|
  arr2 << e.clone
end

but it doesn't seem very elegant, or efficient.

Thanks

dangerousdave
  • 6,331
  • 8
  • 45
  • 62

6 Answers6

12

Your second solution can be shortened to arr2 = arr.map do |e| e.dup end (unless you actually need the behaviour of clone, it's recommended to use dup instead).

Other than that your two solutions are basically the standard solutions to perform a deep copy (though the second version is only one-level deep (i.e. if you use it on an array of arrays of strings, you can still mutate the strings)). There isn't really a nicer way.

Edit: Here's a recursive deep_dup method that works with arbitrarily nested arrays:

class Array
  def deep_dup
    map {|x| x.deep_dup}
  end
end

class Object
  def deep_dup
    dup
  end
end

class Numeric
  # We need this because number.dup throws an exception
  # We also need the same definition for Symbol, TrueClass and FalseClass
  def deep_dup
    self
  end
end

You might also want to define deep_dup for other containers (like Hash), otherwise you'll still get a shallow copy for those.

sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • Thanks for your answer sepp2k, so if you have a nested array, the only way is to use Marshal? – dangerousdave Apr 05 '10 at 19:39
  • @Jon: No you could also define a recursive deep_dup method (see my edit), but using Marshal is usually easier. – sepp2k Apr 05 '10 at 19:42
  • Can't you avoid patching Numeric etc. by defining deep_dup in Object to do this?: respond_to?(:dup) ? dup : self – Lars Haugseth Apr 06 '10 at 14:19
  • @Lars: No, because Numerics do respond to dup. They just respond by throwing an exception. – sepp2k Apr 06 '10 at 14:24
  • @sepp2k: Bummer. Then something like [Numeric, Symbol, TrueClass, FalseClass].include?(self.class) ? self : dup; ought to do the trick. – Lars Haugseth Apr 11 '10 at 15:38
  • @LarsHaugseth: It does if you replace `include?(self.class)` with `any? {|c| self.is_a? c}` or if you add all of the subclasses of Numeric to the array. – sepp2k Apr 11 '10 at 15:48
  • There is a problem with DUP if your array has FixNums like integer - can't dup Fixnum http://stackoverflow.com/questions/1964143/why-dont-numbers-support-dup http://stackoverflow.com/questions/19165957/ruby-cant-dup-fixnum – stack1 Jul 15 '15 at 05:55
  • @stack1 That's why I defined `deep_dup` to return `self` rather than calling `dup` for numbers. – sepp2k Jul 15 '15 at 14:00
7

I am in a similar situation and very concerned about speed. The fastest way for me was to make use of map{&:clone}

So try this:

pry(main)> a = (10000..1000000).to_a.shuffle.map(&:to_s)
pry(main)> Benchmark.ms { b = a.deep_dup }                                                                                     
=> 660.7760030310601
pry(main)> Benchmark.ms { b = a.join("--!--").split("--!--") }
=> 605.0828141160309
pry(main)> Benchmark.ms { b = a.map(&:clone) }
=> 450.8283680770546
Fabian
  • 275
  • 4
  • 8
5

I recommend your initial idea, but written slightly more concisely:

arr = ["red","green","yellow"]
arr2 = arr.inject([]) { |a,element| a << element.dup }
James A. Rosen
  • 64,193
  • 61
  • 179
  • 261
  • 1
    There is a problem with DUP if your array has FixNums like integer - can't dup Fixnum http://stackoverflow.com/questions/1964143/why-dont-numbers-support-dup http://stackoverflow.com/questions/19165957/ruby-cant-dup-fixnum – stack1 Jul 15 '15 at 05:55
3

You can make a deep copy of array a by following code:

 Marshal.load(Marshal.dump(a))
9re
  • 2,408
  • 27
  • 27
2

It looks so simple.. Just run the below code:

a = [1,2,3]
b = [].replace(a)

b[1] = 5

puts a
puts b

Run above code and you will notice the difference. Cheers !

Ankit Vadi
  • 475
  • 4
  • 10
1

You can use this hack:

arr1 = %w{ red green blue }
arr2 = arr1.join("--!--").split("--!--")

But it is just for fun :)

arr2[0].replace("lol")
p arr1
#=> ["red", "green", "blue"]
p arr2
#=> ["lol", "green", "blue"]

And it will work only for 1 level arrays

fl00r
  • 82,987
  • 33
  • 217
  • 237
  • 1
    It will also only work if the array only contains strings and none of the strings contain "--!--" as a substring. – sepp2k Apr 05 '10 at 18:38
  • sepp2k, yeap, it's just hack for one purpose only :) like tetra pack – fl00r Apr 05 '10 at 18:40