結論:
dup
とはオブジェクトをコピーするメソッドです- 細かく言うと「シャロウコピー(Shallow Copy)」します
- コレクション(配列やハッシュ)を
dup
するときは要注意です
破壊的な変更は同じobject_id
を持つオブジェクトに影響が及ぶ
ステップバイステップで理解していくため、まずはこちらを見てください。
# 文字列をorigin変数に代入 > origin = 'hoge' => "hoge" # copy変数にoriginを代入する > copy = origin => "hoge" # copyしたほうをupcase!してみる > copy.upcase! => "HOGE" # originもupcaseされる > origin => "HOGE"
copyをupcase!
(破壊的変更)するとoriginも変更されてしまいました。なぜでしょうか?それは、copyとoriginは同じobject_id
を参照しているからです。つまり、「同一のオブジェクト」と見なされているということです。
# 両者のobject_idは同じ > copy.object_id => 263700 > origin.object_id => 263700 # equal?はobject_idを評価する > copy.equal? origin => true
upcase!
といった破壊的な変更は、同じobject_id
を持つオブジェクトにも影響が及ぶということですね。
dupのチカラ
では、同じobject_id
を持つオブジェクトにも影響が及ぶことを避けたい場合、どうしたらいいのでしょうか?
ここでdup
の出番です。
> origin = 'hoge' => "hoge" # dupを使ってオブジェクトをコピーする > copy = origin.dup => "hoge" > copy.upcase! => "HOGE" # originは変わっていない > origin => "hoge"
上記のように、dup
を使ってコピーすることで、元のオブジェクトであるoriginに影響を与えずに済みました。dup
を使っているため、それぞれのobject_id
も違います。
> origin.object_id => 38760 > copy.object_id => 43980 # equal?はobject_idを評価する > copy.equal? origin => false
良かった〜これでめでたしめでたし…...ではないんです!
配列やハッシュをdupするときの注意点
次はこちらの例を見てください。
# 配列を作る > origin = %w(hoge baz) => ["hoge", "baz"] # dupでコピーする > copy = origin.dup => ["hoge", "baz"] # copyの各要素をupcase!する > copy.map(&:upcase!) => ["HOGE", "BAZ"] # originも変わった!? > origin => ["HOGE", "BAZ"]
あれ!?
dup
を使えばコピー元であるoriginは、コピー先(copy)がたとえ破壊的変更を受けても影響を受けないはずです。それぞれのobject_id
も違います。
> origin.object_id => 78220 > copy.object_id => 83700
ではなぜこんなことが起きるのでしょうか?
それは、dup
が具体的には「シャローコピー(Shallow Copy)」を実行しているからです。シャローコピーをする場合、originのオブジェクトが配列やハッシュだと、中身の要素はコピー先の破壊的変更の影響を受けてしまうのです。
# ハッシュの場合 > origin = { a: 'a', b: 'b' } => {:a=>"a", :b=>"b"} > copy = origin.dup => {:a=>"a", :b=>"b"} # copyの各要素を破壊的変更 > copy.each_value.map(&:upcase!) => ["A", "B"] # originも影響を受ける > origin => {:a=>"A", :b=>"B"}
よって、dup
を使う場合は「オブジェクトのコピー」をしているのではなく、「シャローコピーをしている」という意識を持っておかないと、意図せずバグを生むかもしれません。
シャローコピーに対するディープコピー
シャローコピーのときにコピー元の配列やハッシュの要素が影響を受けないようにするにはどうしたらいいのでしょうか?
そんなときは「Shallow Copy」に対して「Deep copy」という方法を使います。いろいろ方法はありますが、RailsのActiveSupportにはdeep_dup
という名前そのまんまのメソッドが用意されています。
> require 'active_support' => true > require 'active_support/core_ext' => true > origin = %w(hoge baz) => ["hoge", "baz"] # dupではなくdeep_dupを使う > copy = origin.deep_dup => ["hoge", "baz"] > copy.map(&:upcase!) => ["HOGE", "BAZ"] # コピー元は破壊的変更の影響を受けていない! > origin => ["hoge", "baz"]
Rubyを使って仕事をするということは、ほとんどの場合はRailsを使うはずです。今回はRailsのdeep_dup
メソッドを使いましたが、Rubyでディープコピーをしたい場合はMarshal
モジュールを利用できます。
module Marshal (Ruby 3.1 リファレンスマニュアル)
まとめ
- Rubyの
dup
はオブジェクトを「シャローコピー(Shallow Copy)」するメソッド - 配列やハッシュをシャローコピーすると、各要素はコピー先の破壊的変更の影響を受ける
- シャローコピーをしたくないなら、ディープコピーをする
- ディープコピーの方法はいろいろあるが、Railsには
deep_dup
というメソッドが用意されている
関連:
dup
に似たメソッドとしてclone
が存在します。それぞれの違いは以下の記事に書いています。
参考
- [ruby]浅いコピーと深いコピー - Qiita
- ディープコピーの様々な方法を紹介されています