Just do IT

思うは招く

Rubyのdupとは何か

結論:

  • 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」という方法を使います。いろいろ方法はありますが、RailsActiveSupportには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を使うはずです。今回はRailsdeep_dupメソッドを使いましたが、Rubyでディープコピーをしたい場合はMarshalモジュールを利用できます。

module Marshal (Ruby 3.1 リファレンスマニュアル)

まとめ

  • Rubydupはオブジェクトを「シャローコピー(Shallow Copy)」するメソッド
  • 配列やハッシュをシャローコピーすると、各要素はコピー先の破壊的変更の影響を受ける
  • シャローコピーをしたくないなら、ディープコピーをする
  • ディープコピーの方法はいろいろあるが、Railsにはdeep_dupというメソッドが用意されている

関連:

dupに似たメソッドとしてcloneが存在します。それぞれの違いは以下の記事に書いています。

k-koh.hatenablog.com

参考