Ruby

Rubyのdupとは?cloneとの違い・浅いコピーの落とし穴とRailsのdup/deep_dup【Ruby 4.0】

Rubyのdupとは?cloneとの違い・浅いコピーの落とし穴とRailsのdup/deep_dup【Ruby 4.0】

Rubyのdupは、オブジェクトの浅いコピー(インスタンス変数だけを複製し、その参照先は共有するコピー)を返すメソッドです。cloneとの違いは、凍結状態・特異メソッド・extendしたモジュールを引き継ぐかどうかの3点です。ただし、どちらを使ってもネストした配列やハッシュの中身は元と共有されます。RailsではActiveRecordのdupがidのない新規レコードを返し、cloneは元と属性を共有します。この記事では、Ruby 4.0.6とRails 8.1.4で実行したコードを使い、どの場面でどのコピーを選べばよいかを順に説明します。

まとめ:dupとcloneの違いと使い分け

  • dupは浅いコピーを返す。凍結状態・特異メソッド・extendしたモジュールは引き継がない。変更できる複製が欲しいときはこちらを使う。
  • cloneも浅いコピーを返すが、凍結状態・特異メソッド・extendしたモジュールまで引き継ぐ。clone(freeze: false)はRuby 2.4から、非凍結のオブジェクトを凍結して複製するclone(freeze: true)はRuby 3.0から使える。
  • ネストした中身まで分けるなら、標準RubyではMarshal.load(Marshal.dump(obj))を使う。ProcやIO、default procを持つハッシュは複製できない。外部から受け取ったデータには使わない。
  • RailsのActiveRecordは、dupがidなしの新規レコードを返し、保存するとINSERTになる。cloneは元と属性を共有するので、保存すると元の行をUPDATEする。レコードの複製にはdupを使う。
  • deep_dup(ActiveSupport)が中まで複製するのは配列とハッシュだけ。通常の独自クラスではdupに委譲されるため、複製処理を独自に定義していなければインスタンス変数の参照先は共有される。複製不可と判定されるオブジェクトや名前付きのクラス・モジュールは、そのまま返される。

Rubyのdupとは:代入との違いとコピーされる範囲

dupは「duplicate(複製)」を縮めた名前で、レシーバと同じクラスの新しいオブジェクトを作り、インスタンス変数をコピーして返します。Ruby 4.0の公式リファレンスは、dupを「instance variables of obj are copied, but not the objects they reference」と説明しています。つまり、インスタンス変数はコピーされますが、その参照先のオブジェクトはコピーされません。

代入による参照の共有とdupによる複製

b = aのような代入はコピーを作りません。同じオブジェクトに2つ目の名前を付けるだけです。そのため、片方で<<などの破壊的メソッドを呼ぶと、もう片方の値も変わります。元の値を残したまま手を加えたいときは、dupで別のオブジェクトを作ってから変更します。

a = "ruby"
b = a          # 代入(同じオブジェクト)
c = a.dup      # 複製(別のオブジェクト)

b << "!"
a              # => "ruby!"
c              # => "ruby"
a.equal?(b)    # => true
a.equal?(c)    # => false

Integer・Symbol・nilのdupとselfの返却

Integer、Symbol、nil、trueのような即値は、dupしても新しいオブジェクトは作られず、自分自身(self)がそのまま返ります。これらの値にdupを呼んでも例外にはなりません。ただし、すべてのオブジェクトが複製できるわけではなく、ThreadなどではTypeError(allocator undefined for Thread)になります。また、即値はclone(freeze: false)で凍結を外すこともできず、Ruby 4.0.6ではArgumentError(can’t unfreeze Integer)になります。

1.dup.equal?(1)          # => true
:sym.dup.equal?(:sym)    # => true
nil.dup                  # => nil
1.clone(freeze: false)   # => ArgumentError: can't unfreeze Integer

凍結文字列を書き換えたいときのdupと+@

dupは凍結状態を引き継がないため、凍結された文字列から変更できる複製を作る定番の方法です。Ruby 3.4以降は、frozen_string_literalコメントのないファイルの文字列リテラルを書き換えると非推奨の警告が出ます(-W:deprecatedを付けて実行したときに表示されます)。この変更を告知したRuby 3.4.0のNEWSは、str.dup if str.frozen?と書く代わりに単項演算子String#+@を使うよう案内しています。+strは、凍結されていない文字列ならそのまま返し、凍結されている文字列や書き換えると警告が出る文字列なら複製を返します。

s = "draft"           # frozen_string_literalコメントのないファイル
s.frozen?             # => false(Ruby 4.0.6)
s << "!"              # -W:deprecated付きだと「literal string will be frozen in the future」

lit = "x".freeze
(+lit).frozen?        # => false(複製が返る)
lit.dup.frozen?       # => false

dupとcloneの違い:凍結状態・特異メソッド・extendしたモジュール

cloneはオブジェクトを内部状態ごと複製し、dupは元と同じクラスから新しいインスタンスを作ります。Ruby公式リファレンスのKernel#cloneは「copies the frozen value state of obj」と書いており、cloneは凍結状態もコピーします。dupの説明には「any modules that the object has been extended with will not be copied」とあり、extendしたモジュールはdupではコピーされません。

観点 dup clone
コピーの深さ 浅いコピー 浅いコピー
インスタンス変数 コピーする コピーする
凍結状態 引き継がない 引き継ぐ
特異メソッド 引き継がない 引き継ぐ
extendしたモジュール 引き継がない 引き継ぐ
呼ばれるフック initialize_dup initialize_clone
凍結の指定 できない freeze: true/false

特異メソッドとextendしたモジュールの引き継ぎ

特異メソッド(そのオブジェクトだけに定義したメソッド)とextendで追加したモジュールは、どちらも特異クラスに置かれます。cloneは特異クラスごと複製するので、どちらのメソッドも呼べます。dupは特異クラスを複製しないので、どちらも失われます。特異メソッドやextendを使ってオブジェクトごとに振る舞いを変える設計(メタプログラミングの仕組みと実装例で扱う手法)では、複製にdupを使うと振る舞いが元に戻ってしまう点に注意してください。

module Greeting
  def hello = "hello"
end

obj = Object.new
obj.extend(Greeting)
def obj.bye = "bye"

obj.clone.hello                  # => "hello"
obj.clone.bye                    # => "bye"
obj.dup.respond_to?(:hello)      # => false
obj.dup.respond_to?(:bye)        # => false

clone(freeze:)のRuby 2.4・3.0での仕様変更

freeze:キーワード引数はRuby 2.4.0で追加されました(「Kernel#clone now takes an optional keyword argument, freeze flag」)。freeze: trueを渡したとき、元が凍結されていなくても凍結した複製を返すようになったのはRuby 3.0.0からです(「will return a frozen copy even if the receiver is unfrozen」)。同じ3.0で、freeze:の値がinitialize_cloneにも渡されるようになりました。Ruby 2.7以前の環境を対象にするライブラリでは、freeze: trueを渡しても凍結した複製が返る保証はありません。

s = "abc".freeze
s.clone.frozen?                  # => true(既定は元の凍結状態を引き継ぐ)
s.clone(freeze: false).frozen?   # => false(2.4以降)
"abc".clone(freeze: true).frozen?  # => true(3.0以降)

浅いコピーと深いコピー:ネストした配列・ハッシュの独立性

dupもcloneも、複製されるのは一番外側のオブジェクトだけです。Array#dupやHash#dupでも同じで、要素として入っている配列・ハッシュ・文字列は元と同じオブジェクトのままです。設定のハッシュをdupして一部だけ書き換えたつもりでも、ネストした値を変更すると元の設定まで変わります。

dupしたハッシュと元のハッシュの参照共有

config = { db: { host: "localhost" }, tags: ["web"] }
copy = config.dup

copy[:tags] << "api"
copy[:db][:host] = "db.internal"
config  # => {db: {host: "db.internal"}, tags: ["web", "api"]}

ネストが1段だけなら、値ごとにdupすれば分けられます。配列はmap(&:dup)、ハッシュはtransform_values(&:dup)を使います。ただし、これで複製されるのは1段目の値だけです。2段目より深い値は共有されたままです。

config = { db: { host: "localhost" }, tags: ["web"] }
one = config.transform_values(&:dup)
one[:tags] << "api"
config[:tags]   # => ["web"](1段目の値は分かれた)

Marshalで深いコピーを作るときの制約

Ruby本体の機能だけで全階層を複製するなら、Marshal.dumpでバイト列にしてからMarshal.loadで戻します。Ruby 4.0.6で試すと、次のオブジェクトを含む場合はTypeErrorになりました。

config = { db: { host: "localhost" }, tags: ["web"] }
deep = Marshal.load(Marshal.dump(config))
deep[:db][:host] = "db.internal"
config[:db][:host]   # => "localhost"

Marshal.dump(-> {})               # TypeError: no _dump_data is defined for class Proc
Marshal.dump(STDOUT)              # TypeError: can't dump IO
Marshal.dump(Hash.new { [] })     # TypeError: can't dump hash with default proc
o = Object.new; def o.x = 1
Marshal.dump(o)                   # TypeError: singleton can't be dumped

見落としやすいのは、Hash.new { |h, k| h[k] = [] }のようにdefault procを持つハッシュです。集計処理でよく使う形ですが、Marshalでは複製できません。また、同じ配列を2つのキーから参照している場合、Marshalで戻した結果も1つの配列を共有したままになります(m[:a].equal?(m[:b])がtrue)。参照のつながりまで含めて元の構造を再現する、という意味で正確なコピーです。

Marshalを使うときに一番注意すべきなのは安全性です。Marshalの公式ドキュメントには「you should never unmarshal user supplied input or other untrusted data」とあり、信頼できないデータをMarshal.loadするとリモートコード実行につながると警告しています。メモリ上のオブジェクトをMarshal.dumpしてすぐMarshal.loadする使い方なら問題ありません。一方、Cookieやキャッシュ、外部から受け取ったバイト列をMarshal.loadで深いコピーの代わりに戻すのは避けてください。この攻撃の仕組みは安全でないデシリアライゼーションの仕組みと言語別の防御実装で詳しく扱っています。

initialize_copyによる独自クラスの複製範囲の制御

dupとcloneは、複製を作ったあとにinitialize_copyを呼びます。正確には、dupはinitialize_dupを、cloneはinitialize_cloneを呼び、そこからinitialize_copyが呼ばれます。このinitialize_copyをオーバーライドすると、インスタンス変数として持っている配列だけは独立させる、といった制御ができます。オブジェクトの外からMarshalで深いコピーを作るより、そのクラスの中で「どこまで複製するか」を決めておく方が、Procを持つ場合などの例外を避けられます。

class Order
  attr_reader :items

  def initialize(items) = @items = items

  def initialize_copy(source)
    super
    @items = source.items.map(&:dup)
  end
end

o1 = Order.new(["pen"])
o2 = o1.dup
o2.items << "ink"
o2.items[0] << "cil"
o1.items   # => ["pen"](配列も要素の文字列も分かれた)

dupのときだけ処理を変えたい場合はinitialize_dupを、cloneのときだけならinitialize_clone(source, freeze: nil)をオーバーライドします。clone(freeze: true)で呼ぶとfreeze: trueがそのまま渡されるので(Ruby 3.0以降)、凍結されるかどうかに応じて処理を分けられます。オーバーライドした中では、親クラスのフック処理を引き継ぐためにsuperを呼びます。ただし、インスタンス変数のコピーはフックが呼ばれる前に済んでいます。superを省いても、インスタンス変数が複製されなくなるわけではありません。

RailsのActiveRecordにおけるdupとcloneの使い分け

ActiveRecordのモデルでは、dupとcloneの違いがデータベースの主キーに関わるため、使い分けの影響が大きくなります。Rails 8.1のActiveRecord::CoreのAPIドキュメントは、dupを「Duped objects have no id assigned and are treated as new records」と説明しています。cloneについては「modifying attributes of the clone will modify the original」と書いています。モデル全体の仕組みはActive Recordとは?Railsを支える設計パターンを参照してください。次の比較表とコード例は、保存済みのレコードを対象にしています。未保存のレコードをcloneした場合は新規レコードのままで、保存するとINSERTになります。

観点 ActiveRecord#dup ActiveRecord#clone
id nil 元と同じ
new_record? true false
save時のSQL INSERT(新しい行) UPDATE(元の行)
created_at/updated_at nil(保存時に設定) 引き継ぐ
属性の変更 元に影響しない 元も変わる
関連(has_many等) 複製しない 複製しない

dupによる新規レコードの作成

Rails 8.1.4とSQLite(メモリ上のDB)で確かめると、dupしたレコードはidとcreated_atがnilになり、save!すると新しい行としてINSERTされました。APIドキュメントのとおり、標準のタイムスタンプ列(created_at、created_on、updated_at、updated_on)と楽観ロック用の列もコピーされません。投稿を下書きとして複製する、テンプレートのレコードから新しいレコードを作る、といった用途にはこのdupを使います。

以下はRails consoleで実行する例です。postsテーブルにtitle・created_at・updated_at列があり、Postモデルが定義済みであることを前提とします。後続の関連複製例では、Postのhas_many :comments、Commentのbelongs_to :post、commentsテーブルのpost_id列も必要です。

post = Post.create!(title: "orig")
copy = post.dup
copy.id            # => nil
copy.new_record?   # => true
copy.created_at    # => nil
copy.title = "copy"
post.title         # => "orig"
copy.save!         # INSERTされ、Post.countが1増える

保存済みレコードのcloneによる属性共有と元の行の更新

cloneしたレコードはidが元と同じで、persisted?がtrueのままです。属性も元と共有しているため、cloneしたレコードのtitleを変えると元のオブジェクトのtitleも変わります。その状態でsave!すると、元の行がUPDATEされます。「複製してから中身を書き換えて保存する」という処理をcloneで書くと、新しい行はできず、元のデータが上書きされます。ActiveRecordのレコードを複製するときにcloneを使う場面はほとんどありません。

c = post.clone
c.id                 # => post.idと同じ
c.title = "via clone"
post.title           # => "via clone"(元のオブジェクトも変わる)
c.save!              # 元の行をUPDATE(行数は増えない)

has_manyの子レコードを含む複製

APIドキュメントには「not its associations」とあり、dupは関連先のレコードを複製しません。どこまで複製するかはアプリケーションが決めることだ、という立場です。子レコードも一緒に複製したい場合は、子をそれぞれdupして新しい親に入れてから保存します。関連の定義方法はRailsのAssociationとは?6種類の関連付けと使い分けにまとめています。

new_post = post.dup
new_post.comments = post.comments.map(&:dup)
new_post.save!   # 親と子がそれぞれINSERTされる

複製したいモデルが多い場合や、関連が何段も続く場合は、gemで宣言的に書く方法があります。deep_cloneable(3.2.2、2026年2月19日公開)とamoeba(3.4.1、2026年10月18日公開)がこの用途の代表的なgemです(版はrubygems.orgのAPIで2026年10月27日に確認)。どちらを使う場合も、一意制約のある列(スラッグや外部ID)は複製後に値を書き換える必要があります。書き換えないと保存時に制約違反になります。

ActiveSupportのdeep_dupが再帰する範囲

Railsでネストしたハッシュを複製するときによく使われるのが、ActiveSupportのdeep_dupです。Rails 8.1.4のソース(active_support/core_ext/object/deep_dup.rb)では、中身まで再帰して複製するのはArray#deep_dup(要素ごとにdeep_dup)とHash#deep_dup(値と、String・Symbol以外のキーにdeep_dup)の2つです。String・Symbolのキーにはdeep_dupを呼びません。それ以外のオブジェクトのObject#deep_dupは「duplicable? ? dup : self」で、複製可能ならdup、そうでなければselfを返します。名前付きのクラス・モジュールにはModule#deep_dupが適用され、こちらもselfが返ります。名前から想像するほど深くは複製しません。

require "active_support/all"

class Box
  attr_reader :items
  def initialize = @items = [1]
end

b = Box.new
c = b.deep_dup
c.items << 2
b.items          # => [1, 2](独自クラスの中身は共有されたまま)

h = { a: { b: [1] } }
h.deep_dup[:a][:b] << 2
h                # => {a: {b: [1]}}(ハッシュと配列は中まで分かれる)

この仕組みから、次の3つの挙動が起きます。どれもRails 8.1.4で確かめたものです。

  • ハッシュの中のActiveRecordは新規レコードになる:{ record: post }.deep_dup[:record].idはnilになります。中のレコードにはdupが呼ばれ、id無しの新規レコードとして複製されるためです。paramsと一緒にレコードを入れたハッシュをdeep_dupしてから保存処理に渡すと、更新のつもりがINSERTになります。
  • 共有していた参照が分かれる:同じ配列を2つのキーから参照しているハッシュをdeep_dupすると、それぞれ別の配列になります。Marshalは共有を保ったまま複製するので、ここは結果が異なります。
  • 凍結が外れる:凍結したハッシュや文字列をdeep_dupすると、複製は凍結されていません。一方、default procを持つハッシュはHash#dupがdefault procを引き継ぐので、Marshalと違って複製できます。
観点 dup deep_dup Marshal
ネストした配列・ハッシュ 共有 複製 複製
独自クラスの中身 共有 共有 複製
Proc・IOを含む 可 可 TypeError
default proc付きハッシュ 可 可 TypeError
共有している参照 共有のまま 分かれる 共有のまま
必要なもの 標準 ActiveSupport 標準

dup・clone・deep copyを選ぶ判断と避ける場面

迷ったときは、次の順で決めると間違いが起きにくくなります。

  • 基本はdup:変更できる複製が欲しいならdupを使います。凍結された値を書き換え用に複製する場合もdup(文字列なら+str)です。
  • cloneは元の性質を保ちたいときだけ:特異メソッドやextendしたモジュールを持つオブジェクト、凍結したまま扱いたい値オブジェクトを複製するときに使います。ActiveRecordのレコードには使いません。
  • ネストの中身まで書き換えるなら深いコピー:Railsで扱うのが配列とハッシュだけならdeep_dup、標準Rubyで信頼できるメモリ上のデータならMarshal、独自クラスならinitialize_copyで複製する範囲を決めます。

深いコピーが必要になるのは、そもそも複製したデータを書き換えるからです。書き換えないのであれば、コピーするより凍結する方が確実です。Ruby 3.2以降のDataはインスタンスが最初から凍結されており、point.with(y: 5)のように一部の値だけ変えた新しいインスタンスを作れます。ただし、凍結されるのはインスタンス自身だけで、メンバーに入れた配列や文字列は凍結されません(Ruby 4.0.6でd.list.frozen?はfalse)。ネストしたデータを丸ごと凍結したい場合はRactor.make_shareable(obj)が使え、Ruby 4.0.6ではハッシュの中の配列や文字列まで凍結されました。設定値や定数のように「受け取った側に変更させたくない」データは、dupで守るより凍結しておく方が、複製の漏れによる不具合が起きません。Dataなど、現行版で使える構文や変更点はRubyのバージョンと変更点で確認できます。

よくある質問

Rubyのdupとはどういう意味ですか?

duplicate(複製)を縮めた名前のメソッドで、オブジェクトの浅いコピーを返します。インスタンス変数はコピーされますが、その参照先(ネストした配列やハッシュ)は元と共有されます。凍結状態と特異メソッドは引き継ぎません。

dupとcloneはどちらを使えばよいですか?

通常はdupを使います。cloneは凍結状態・特異メソッド・extendしたモジュールまで引き継ぐので、それらを保ったまま複製したいときだけ使います。どちらも浅いコピーである点は同じです。

配列をcloneすると中の要素も複製されますか?

複製されません。Array#cloneもArray#dupも、複製されるのは配列そのものだけです。要素の文字列を破壊的に変更すると元の配列の要素も変わります。要素も分けたいならmap(&:dup)、2段以上ネストしているならMarshalかdeep_dupを使います。

RailsでdupしたレコードのIDはどうなりますか?

idはnilになり、new_record?がtrueの新規レコードとして扱われます。saveすると新しいidでINSERTされます。created_at/updated_atはコピーされず、関連先のレコードも複製されません。

deep_dupとMarshalのどちらで深いコピーをすべきですか?

Railsで扱うデータが循環参照のない配列とハッシュで、共有参照が別々のコピーに分かれてもよいなら、deep_dupを使えます。自分自身を含む配列などに使うと、再帰が止まらずSystemStackErrorになります。default procを持つハッシュやProcを含んでいても複製できます。独自クラスの中身まで複製したい場合はMarshalかinitialize_copyを使います。ただし、外部から受け取ったデータをMarshal.loadしてはいけません。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2025.02.27 コラム トラッキングビーコン(Webビーコン)とは?仕組み・用途・確認と対策を徹底解説
  2. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  3. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  4. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  5. 2026.10.21 テックブログ ユミルリンク不正アクセス第二報で延べ5,162万件:Cuenote FC利用企業と受信者の確認点

RELATED POSTS 関連記事

目次