RubyのArray#dupで引っかかった

その引っかかった原因から。

2次元配列へのArray#map!

あるテストケースにおいて、予期されるオブジェクトを作るために

  • 元のオブジェクトをdupでコピー
  • コピーしたオブジェクトに色々する
  • メソッドの適用結果と比較

とするはずだったのに、結果がおかしい。
ここでそのコードを簡単にしてみたものと、実行結果を示してみる。
UbuntuのRVM上のRuby1.9.2-p180で実行している。

# -*- coding: utf-8 -*-
a = [1, 2]
b = a.dup
b[2] = [3]
c = [['1', '2'], ['3', '4']]
d = c.dup
d[2] = ['5', '6']

print "Array#map実行前\n"
print "a: #{a}\n"
print "b: #{b}\n"
print "a.equal?(b): #{a.equal?(b)}\n"
print "c: #{c}\n"
print "d: #{d}\n"
print "c.equal?(d): #{c.equal?(d)}\n\n"

b.map!{|n| n.to_s}
d.map!{|ary| ary.map!{|n| n.to_i * 2}}

print "Array#map!実行後\n"
print "a: #{a}\n"
print "b: #{b}\n"
print "c: #{c}\n"
print "d: #{d}\n"

実行結果

Array#map実行前
a: [1, 2]
b: [1, 2, [3]]
a.equal?(b): false
c: [["1", "2"], ["3", "4"]]
d: [["1", "2"], ["3", "4"], ["5", "6"]]
c.equal?(d): false

Array#map!実行後
a: [1, 2]
b: ["1", "2", "[3]"]
c: [[2, 4], [6, 8]]
d: [[2, 4], [6, 8], [10, 12]]


同様のものをideone.comにも書いてみました。
http://ideone.com/SnuXu

結果

equal?メソッドでオブジェクトは同じものを指していないことを確認している。
しかしdupメソッドを適用した2次元配列のArrayオブジェクトにmap!を実行すると、dup元のオブジェクトにもmap!が適用されている。
map!を使わずに代入を用いると、dのみにmapが適用されて意図した結果が返ってくることが分かる。

その結果(ちゃんと)調べたのがArray#dup

Array#dupの挙動

RubyリファレンスマニュアルのArray#dupの項を引用すると

レシーバと同じ内容を持つ新しい配列を返します。
clone は frozen tainted singleton-class の情報も含めてコピーしますが、 dup は内容だけをコピーします。またどちらのメソッドも要素それ自体のコピーはしません。つまり「浅い(shallow)」コピーを行います。

ary = ['string']
p ary             #=> ["string"]
copy = ary.dup
p copy            #=> ["string"]

ary[0][0...3] = ''
p ary             #=> ["ing"]
p copy            #=> ["ing"]

どちらのメソッドも要素それ自体のコピーはしませんとある。

だからリファレンスマニュアルではaryを変化させるとcopyも変化するのか。
自分がちゃんと読まずに内容だけをコピーします、だけに注意してたせいですね。


まとめ

  • 2次元配列や、文字列を含んだ配列でArray.dupは気をつけよう
  • リファレンスちゃんと読め