コピー先の配列の値を書き換えようとしたら、なぜか、コピー元の方まで値が書き変わってしまう謎現象に遭遇した。
困ったのでメモ。
間違っている箇所あったらコメントで教えてね。
上書きどうこうの前に、まず以下3概念を理解する必要があるみたいです。
・浅いコピー:shallow copy
・深いコピー:deep copy
・配列の参照渡し
【Ruby】浅い頭を悩ませる「浅いコピー」
- 浅いコピー:
dup
とclone
メソッド
浅いコピーというのは、コピー元のオブジェクトとコピー先のオブジェクトがメモリ上の同一の場所を参照するコピーです。
(?????!!?)
と言われても、なんのこっちゃと思う人も多いかと思うのですが、まあ、実際の挙動としては「浅いコピー」は全てをコピーしきれないコピーでした(説明雑w)。
Linuxで言う、シンボリックリンクとハードリンクって考えるとわかりやすいかも。
ふむ…(困惑)
そのため、片方に変更を加えると、もう片方のデータも変わってしまいます。これは落とし穴…。
配列の参照渡し
あと、配列の参照渡しなるものも気をつけなきゃいけないらしい。
厳密にはRubyは「参照渡し」ではなく「参照の値渡し」らしいですが、初心者はひとまず「参照渡し」と解釈しても良いのではないかと個人的には思います。興味ある方はこちらの記事、値渡しと参照渡しの違いを理解するをご覧ください。
A=BでBをコピーできるのかと思っていたが、そうじゃないらしい。
# 配列データを準備
$ origin = ["書き換え前"]
=> ["書き換え前"]
# コピーするよ!!
$ copy = origin
=> ["書き換え前"]
# コピーした(はずの)配列を書き換えてみる
$ copy[0] = "書き換えたよ!!!!"
=> "書き換えたよ!!!!"
# ↓なんでだよ!!!(なぜか元データが変わってる)
$ origin
=> ["書き換えたよ!!!!"]
# オブジェクトIDを比較してみる
$ origin.object_id
=> 440
# あれ?オブジェクトIDが一緒...?コピーできてないっぽい
$ copy.object_id
=> 440
そもそもA=BでBはコピーできないらしい。
オブジェクト名は違うが、結局同じ住所を見ているから、だと。
また、よく使うコピー関連のメソッドにcloneがありますよね。
cloneでコピーすれば別物な気もするが…
cloneでコピーすれば、object_idが異なるオブジェクトが出来上がります。ので、一見、ちゃんとコピーされている(別物として作られている)気がしてしまいます。しかし、注意点があります。
cloneは1次元までは問題なくコピーされるものの、2次元以降はコピーされません。
◎コピーされる:a = [“hello”]
×コピーされない:a = [[“hello”]]
×コピーされない:a = [[[“hello”]]]
cloneを使用する際は、生成オブジェクトがハッシュや配列などの階層構造である場合に、意図した挙動にならない可能性があるので、注意が必要です、とのこと。
# 多次元の元データを準備
$ origin = ["hello", ["hoge"], ["fuga"]]
=> ["hello", ["hoge"], ["fuga"]]
# コピー作成
$ copy = origin.clone
=> ["hello", ["hoge"], ["fuga"]]
# 元データの配列を変更
$ origin[0] = "hello change!!"
=> "hello change!!"
$ origin[1][0] = "hoge change!!"
=> "hoge change!!"
$ origin[2][0] = "fuga change!!"
=> "fuga change!!"
# 確認すると...
$ origin
=> ["hello change!!", ["hoge change!!"], ["fuga change!!"]]
$ copy
=> ["hello", ["hoge change!!"], ["fuga change!!"]]
多次元配列の場合は、1次元目はしっかりコピー(別物扱い)されているのに、2次元以降はコピーされず、元データと同一値になってしまっています。
なぜ、このような挙動になっているのかはわかりませんが、バグの原因になりそうなので気をつけないといけませんね。
ただ、このcloneメソッドって使うメリットあるの…?と感じてしまったのですが、深いコピーはメモリを食う一方、浅いコピーはメモリ節約になるというメリットがあるようです。
【Ruby】全部コピー=深いコピー
一方のディープな方のコピー。
1次元目だけでなく、2次元以降も根こそぎ完全なるコピー(別物)を作りたい時です。
$ origin = ["hello", ["hoge"], ["fuga"]]
=> ["hello", ["hoge"], ["fuga"]]
# ↓これで完全なるコピーができる!!!
$ copy = Marshal.load(Marshal.dump(origin))
=> ["hello", ["hoge"], ["fuga"]]
$ copy[0] = "hello change!!"
=> "hello change!!"
$ copy[1][0] = "hoge change!!"
=> "hoge change!!"
$ copy[2][0] = "fuga change!!"
=> "fuga change!!"
$ origin
=> ["hello", ["hoge"], ["fuga"]]
$ copy
=> ["hello change!!", ["hoge change!!"], ["fuga change!!"]]
こちらは、コピーすると完全なる別物になるので、片方の値を書き換えようが、もう片方のデータには影響がありません。
そして、オブジェクトIDを確認すると、もちろん別物になっています。
$ origin.object_id
=> 400
$ copy.object_id
=> 420
まとめ
C言語について多少知っている方は、ポインタ(参照渡し)といった概念があるので、なんとなく理解はしやすいかもしれませんね。一方で、Rubyが初って方は、ちょっとイメージしづらいかもしれません…。
これを知らないと実装でバグの原因になりかねないので、気をつけたいところです。
区別して使えるようになるとベストではありますね。