【Ruby】コピー時に元データがなぜか上書きされる【浅いコピー】

コピー先の配列の値を書き換えようとしたら、なぜか、コピー元の方まで値が書き変わってしまう謎現象に遭遇した。

困ったのでメモ。

エンジニア|ポートフォリオ
新人

間違っている箇所あったらコメントで教えてね。

上書きどうこうの前に、まず以下3概念を理解する必要があるみたいです。

理解すべき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次元以降はコピーされません。

clone は1次元分しかコピーされない

◎コピーされる: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が初って方は、ちょっとイメージしづらいかもしれません…。

これを知らないと実装でバグの原因になりかねないので、気をつけたいところです。

区別して使えるようになるとベストではありますね。

参考

この記事を書いた人

竹田奈央

石川県出身 / 都内在住 / 国立大中退→情報工学科卒 / フリーランスの女性WEBエンジニア / フルリモート / アラサー / 独身 / #ADDress ワーケーション / 松本人志・千鳥好き/ 下記Twitter OR LinkedInボタンで繋がってくれると嬉しいです