読者です 読者をやめる 読者になる 読者になる

simanのブログ

ゆるふわプログラマー。競技プログラミングやってます。Ruby好き

Rubyで二次元配列のコピー

Rubyで二次元配列のコピーを行おうとするとコピー元を破壊してしまうケースが多々あるので、まとめてみた。

・普通にコピー

a = [[1,2],[3,4]]
b = a

b[0][0] = 10  

puts "a = #{a}"
puts "b = #{b}"
a = [[10, 2], [3, 4]]
b = [[10, 2], [3, 4]]

このようにbを変更したつもりがaにも影響が出ていることがわかる。これは「b = a」が配列のコピーではなくて「aの参照先をbにコピー」となっているからである。これは、a, bそれぞれのobject_idを出力することで確認することができる。


object_idを出力

a = [[1,2],[3,4]]
b = a

puts "a object_id = #{a.object_id}"
puts "b object_id = #{b.object_id}"
a object_id = 70256915844520
b object_id = 70256915844520

出力されるidが一緒になっていることがわかる。つまり、aとbは同じ参照先を見ているので片方の変更がもう片方の変更に影響されるのである。


dupメソッドでコピー

Rubyにはオブジェクトをコピーするためのdupメソッドが存在する(cloneも同様)。これを使えば別オブジェクトとしてコピーできるのだが、2次元配列では普通にやろうとするとうまくいかない。

・よくある間違い

a = [[1,2],[3,4]]
b = a.dup  #=> これではうまくいかない。

b[0][0] = 10  

puts "a = #{a}"
puts "b = #{b}"
a = [[10, 2], [3, 4]]
b = [[10, 2], [3, 4]]

dupメソッドを使用したので、一見うまく行ったようにみえるのだが、出力結果をみてみると、コピー元を破壊していることがわかる。なぜかというと、dupメソッドによってaとbのobject_idは別々になったのだが、要素の参照先は別オブジェクトになっていないからである。

・object_idを出力

a = [[1,2],[3,4]]
b = a.dup

puts "a object_id = #{a.object_id}"
puts "a[0] object_id =  #{a[0].object_id}"
puts "a[1] object_id =  #{a[1].object_id}"

puts

puts "b object_id = #{b.object_id}"
puts "b[0] object_id =  #{b[0].object_id}"
puts "b[1] object_id =  #{b[1].object_id}"
a object_id = 70356409379120
a[0] object_id =  70356409379160
a[1] object_id =  70356409379140

b object_id = 70356409379100
b[0] object_id =  70356409379160
b[1] object_id =  70356409379140

結局aとbの要素は同じオブジェクトを参照しているので、片方の変更がもう片方に影響してしまう。


じゃあどうするのか

二次元配列を深いコピーするには単にdupを使うだけではだめ。そこでよく出てくるのがMarshalモジュールを使って一度別オブジェクトに変換したあと再度読み戻す方法。

・Marshalを使用

a = [[1,2],[3,4]]
b = Marshal.load(Marshal.dump(a)) #=> 一度dumpして変換した後、再度読み戻す。

b[0][0] = 10  

puts "a = #{a}"
puts "b = #{b}"
a = [[1, 2], [3, 4]]
b = [[10, 2], [3, 4]]

出力結果からコピー元を破壊していないことが確認できる。試しにobject_idを出力してみると、要素のidも別々になっていることがわかる。

・object_idを出力

a = [[1,2],[3,4]]
b = Marshal.load(Marshal.dump(a))

puts "a object_id = #{a.object_id}"
puts "a[0] object_id =  #{a[0].object_id}"
puts "a[1] object_id =  #{a[1].object_id}"

puts

puts "b object_id = #{b.object_id}"
puts "b[0] object_id =  #{b[0].object_id}"
puts "b[1] object_id =  #{b[1].object_id}"
a object_id = 70326423832820
a[0] object_id =  70326423832860
a[1] object_id =  70326423832840

b object_id = 70326423832740
b[0] object_id =  70326423832720
b[1] object_id =  70326423832700


ちゃんと要素まで別オブジェクトとしてコピーできている。


ほかの手法(map)

他の手法としては配列をmapで繰り返し処理しながらそれぞれの要素でdupを行う方法がある

a = [[1,2],[3,4]]
b = a.map(&:dup)

b[0][0] = 10  

puts "a = #{a}"
puts "b = #{b}"
a = [[1, 2], [3, 4]]
b = [[10, 2], [3, 4]]

これでも可能だが、配列の要素が配列以外の場合は処理を分けて記述しないとエラーが発生する場合があるので注意が必要。(要素にdupメソッドが存在しない場合にエラー)


さらに他の手法(eval)

他にも配列をto_sメソッドで文字列に変換した後、evalで実行することで深いコピーを行う方法もある。

a = [[1,2],[3,4]]
b = eval(a.to_s)

b[0][0] = 10  

puts "a = #{a}"
puts "b = #{b}"
a = [[1, 2], [3, 4]]
b = [[10, 2], [3, 4]]

これもちゃんと深いコピーができている。


それぞれを比較してみた

上で紹介した手法でそれぞれベンチマークを取ってみた。とりあえず100万回の二次元配列のコピーを行なってどれだけの実行速度の差が出るかをチェック。

require 'benchmark'

a = [[1,2],[3,4]]

Benchmark.bm do |x|
  x.report do
    1000000.times do
      b = Marshal.load(Marshal.dump(a))
    end
  end
  x.report do
    1000000.times do
      b = eval(a.to_s)
    end
  end
  x.report do
    1000000.times do
      b = a.map(&:dup)
    end
  end
end

実行結果

       user     system      total        real
   3.870000   0.040000   3.910000 (  3.915329)  #=> Marshal
   9.680000   0.140000   9.820000 (  9.814530)  #=> eval
   0.710000   0.000000   0.710000 (  0.704145)  #=> map

evalとMarshalがmapに比べて遅いことがわかる。一度何か別のオブジェクトに変換して読み込む手法はあまりよろしくない感じ。