Rubyのdupとcloneの違いと使い分け|浅い・深いコピーとRailsのdeep_dupまで
Rubyでオブジェクトを複製するdupとcloneは、どちらも「浅いコピー」を返す点は同じですが、frozen状態と特異メソッドを引き継ぐかどうかで挙動が分かれます。この違いを取り違えると、コピーしたつもりの配列やハッシュが元の値まで書き換えてしまう不具合につながります。この記事では、dupとcloneの差、参照が共有される浅いコピーの落とし穴と深いコピーの作り方、配列・ハッシュの複製、そしてRails(ActiveRecord)で挙動が変わるdup/cloneとdeep_dupまでを、実際のコードで整理します。
まとめ:dupとcloneの使い分けと結論
- dup:浅いコピーを返す。frozen状態と特異メソッドは引き継がない。素直な複製が欲しいときの既定。
- clone:浅いコピーを返す。frozen状態と特異メソッドを引き継ぐ。Ruby 2.4以降は
clone(freeze: false)で凍結を外せる。 - 浅いコピーの限界:どちらもネストした配列・ハッシュの中身は参照を共有する。中身まで独立させたいなら深いコピー(
Marshal.load(Marshal.dump(obj)))を使う。 - Rails:
ActiveRecord#dupはidがnilの新規レコード、#cloneは同じidを共有する。レコード複製にはdup、ネストごと複製するならdeep_dup。
dupメソッドとは|オブジェクトの浅いコピーを作る
dupは、レシーバと同じクラスの新しいインスタンスを作り、インスタンス変数をコピーして返します。返るのは元とは別のオブジェクトで、object_idは一致しません。ただしコピーされるのは「そのオブジェクト自身」だけで、frozen状態や特異メソッドは持ち越しません。
original = { name: "Ruby", tags: ["lang"] }
copy = original.dup
copy.object_id == original.object_id # => false(別オブジェクト)
copy[:name] = "Crystal"
original[:name] # => "Ruby"(トップレベルは独立)
凍結した文字列をdupすると、コピーは凍結が外れて再び変更できるようになります。「凍結済みの値をベースに、変更可能な複製が欲しい」ときの定番です。
frozen = "immutable".freeze
frozen.frozen? # => true
frozen.dup.frozen? # => false
dupとcloneの違い|frozen状態と特異メソッドの引き継ぎ
両者の分岐点は「オブジェクトに後付けした状態を引き継ぐか」です。cloneはfrozen状態と特異メソッドまで含めて複製し、dupはそれらを落とします。
| 観点 | dup | clone |
|---|---|---|
| コピーの種類 | 浅いコピー | 浅いコピー |
| インスタンス変数 | コピーする | コピーする |
| frozen状態 | 引き継がない | 引き継ぐ |
| 特異メソッド | 引き継がない | 引き継ぐ |
| 凍結の制御 | 常に非凍結 | freeze: true / false で指定可 |
特異メソッド(そのオブジェクトだけに定義したメソッド)を持たせた場合、cloneは呼び出せますがdupはNoMethodErrorになります。
obj = Object.new
def obj.greet = "hello"
obj.clone.greet # => "hello"
obj.dup.greet # => NoMethodError
Ruby 2.4以降のcloneはキーワード引数freeze:を取れます。clone(freeze: false)なら凍結オブジェクトから「特異メソッドを保ったまま変更可能な複製」を作れ、clone(freeze: true)なら元が非凍結でも凍結した複製を返します。
s = "abc".freeze
s.clone.frozen? # => true(既定は元を踏襲)
s.clone(freeze: false).frozen? # => false
浅いコピーと深いコピー|dup/cloneで参照が共有される落とし穴
dupもcloneも浅いコピーです。トップレベルのオブジェクトは複製されますが、その中に入っている配列やハッシュ、文字列などの要素は元と同じオブジェクトを指したままです。そのため、コピー側でネストした中身を破壊的に変更すると、元のオブジェクトにも影響します。
original = { list: [1, 2, 3] }
copy = original.dup
copy[:list] << 4
original[:list] # => [1, 2, 3, 4](中身の配列は共有されている)
中身まで完全に独立させたい場合は深いコピー(ディープコピー)が必要です。Ruby標準ライブラリだけで作るならMarshalで一度シリアライズして復元する方法が確実です。
deep = Marshal.load(Marshal.dump(original))
deep[:list] << 99
original[:list] # => [1, 2, 3](影響しない)
ただしMarshalには制約があります。Procやlambda、IO・ソケットなどダンプできないオブジェクトを含むとTypeErrorになり、特異メソッドも復元されません。
Marshal.dump(-> { }) # => TypeError: no _dump_data is defined for class Proc
こうしたオブジェクトを含む構造では、対象クラスに複製ロジックを自前で用意するか、後述のRailsのdeep_dupを使います。
配列・ハッシュ・文字列を複製するときの実務
コレクションの複製でも「浅いコピー」であることは変わりません。配列やハッシュをdupしても、要素そのものは共有されます。
words = ["ab", "cd"]
copied = words.dup
copied << "ef" # 配列の構造変更は元に影響しない
words # => ["ab", "cd"]
copied[0].upcase! # 要素の文字列は同じオブジェクト
words[0] # => "AB"(元も変わる)
要素の破壊的変更まで避けたいなら、要素ごとに複製する(words.map(&:dup))か、Marshalで深いコピーにします。凍結した配列を扱う場面では、dupは非凍結の複製を返し、cloneは凍結を保ったまま複製する違いも押さえておくと、意図せず変更可能になった/できなかったという事故を防げます。文字列リテラルの凍結挙動はRubyのバージョンやfrozen_string_literalマジックコメントに左右されるため、扱うRubyの仕様はRuby 4系の最新安定版の解説もあわせて確認してください。
RailsのActiveRecordとdeep_dup|dupが新規レコード・cloneが同idになる理由
Rails(ActiveRecord)では、モデルオブジェクトに対するdupとcloneが標準Rubyと同じ発想でありながら、データベースの主キーが絡むぶん実務上の意味が大きく変わります。Rails APIでレコードを扱う実装でも、複製の選択を誤ると既存データを上書きしかねません。
| 観点 | ActiveRecord#dup | ActiveRecord#clone |
|---|---|---|
| id | nil(新規レコード扱い) | 元と同じidを共有 |
| 保存時の挙動 | 新しい行としてINSERT | 既存の行をUPDATEし上書き |
| タイムスタンプ | コピーせず保存時に再設定 | 引き継ぐ |
| 属性の独立性 | 複製側の変更は元に影響しない | 属性ハッシュを共有し互いに影響しうる |
| 関連(associations) | 複製しない | 複製しない |
つまり「同じ内容の新しいレコードを作りたい」ならdupが正解です。cloneはidを引き継ぐため、複製したつもりでsaveすると元のレコードを更新してしまいます。レコードの複製には原則dupを使い、cloneは避けるのが安全です。
new_user = user.dup # id=nil の新規レコード
new_user.new_record? # => true
new_user.save # 別レコードとしてINSERT
ネストした属性やハッシュ・配列カラムまで含めて独立に複製したいときは、ActiveSupportが提供するdeep_dupを使います。deep_dupは配列・ハッシュを再帰的に複製し、要素の参照共有を断ち切ります。Marshalと違いProcを含んでいてもエラーにならず、Rails環境で扱いやすいのが利点です。
config = { roles: ["admin"], meta: { active: true } }
copied = config.deep_dup
copied[:roles] << "guest"
config[:roles] # => ["admin"](ネストも独立)
dup・clone・deep copyの使い分けと失敗パターン
複製の手段は「どこまで独立させたいか」で選びます。ここまでの内容を判断基準に落とすと次のとおりです。
- 単純な複製が欲しい → dup。frozenや特異メソッドを引きずらないぶん予測しやすく、既定はこれで十分。
- frozen状態や特異メソッドまで保ちたい → clone。凍結を外したいなら
clone(freeze: false)。 - ネストの中身まで独立させたい → 深いコピー。Rails外は
Marshal、Rails内はdeep_dup。 - ActiveRecordのレコード複製 → dup一択。
cloneはidを共有し既存レコードを上書きする危険がある。
典型的な失敗パターンは3つに集約されます。第一に、浅いコピーで済ませてネストした配列・ハッシュを破壊的に変更し、元データまで壊すケース。第二に、凍結文字列をcloneで複製して「なぜか変更できない」と詰まるケース(dupかclone(freeze: false)にする)。第三に、ActiveRecordでcloneしたレコードをsaveして既存行を上書きするケースです。いずれも「dup/cloneは浅いコピー」「cloneは状態を引き継ぐ」「AR#cloneはidを共有する」という3点を押さえれば避けられます。
よくある質問
dupとは何ですか?
オブジェクトの浅いコピーを作るRubyの標準メソッドです。同じクラスの新しいインスタンスを返し、インスタンス変数をコピーします。frozen状態や特異メソッドは引き継ぎません。
dupとcloneはどちらを使うべきですか?
frozen状態や特異メソッドを保つ必要がなければdupで十分です。凍結状態や後付けした特異メソッドまで含めて忠実に複製したい場合や、凍結の有無を明示的に制御したい場合にclone(必要ならclone(freeze: false))を使います。
Rubyでディープコピー(深いコピー)を作るには?
標準ライブラリだけならMarshal.load(Marshal.dump(obj))で、ネストした中身まで独立した複製を作れます。ただしProcやIOを含むオブジェクトはダンプできずTypeErrorになります。Rails環境ではActiveSupportのdeep_dupが使えます。
配列やハッシュをdupしても中身が一緒に変わるのはなぜですか?
dupは浅いコピーで、配列やハッシュの要素は元と同じオブジェクトを参照したままだからです。要素の破壊的変更まで避けたい場合は、要素ごとに複製するか深いコピーを使います。
Railsでレコードを複製するときdupとcloneのどちらが安全ですか?
dupです。dupはidがnilの新規レコードを返すため、保存すると別の行として作成されます。cloneは同じidを共有し、保存すると既存レコードを上書きするため、複製目的には向きません。