Code Explain

Geminiの鋭い視点と分かりやすい解説で、プログラミングスキルを向上させましょう!

【Ruby配列複製】知っておくべき全知識:dup clone からディープコピーまで徹底解説

Rubyで配列を扱う際、あなたは「あれ?元の配列まで変わっちゃった!」「意図しないところでデータが破壊されてる?」といった経験はありませんか? それは、Rubyにおける配列の複製が、単なる「コピー&ペースト」とは一味違う概念を持っているからです。

この記事は、Rubyの配列複製に関するあらゆる疑問を解消し、あなたが自信を持って配列を操作できるようになるための完全ガイドです。シャローコピーとディープコピーの違い、dupclone、そしてdeep_dupといったメソッドの正しい使い方、さらには多次元配列や複雑なオブジェクトを含む配列を扱う際の落とし穴と解決策まで、徹底的に解説していきます。

この記事を読めば、以下の知識が手に入ります。

  • Rubyにおけるオブジェクトと参照の基本
  • シャローコピー(浅いコピー)の仕組みとその落とし穴
  • Array#dupArray#cloneの正確な違いと使い分け
  • ディープコピー(深いコピー)の必要性と実現方法
  • Railsのdeep_dupメソッドの活用法
  • 実務で配列を複製する際のベストプラクティスと注意点

さあ、Rubyの配列複製にまつわる謎を解き明かし、より堅牢でバグの少ないコードを書くための第一歩を踏み出しましょう!


1. なぜ「配列の複製」がそこまで重要なのか?

プログラミングにおいて、データを扱うのは日常茶飯事です。特に配列は、データのリストを管理するための非常に強力なツール。しかし、この配列を何の気なしに「コピー」しているつもりでいると、思わぬバグや予期せぬ挙動に遭遇することがあります。

例えば、以下のようなシナリオを想像してみてください。

  1. ある配列からデータを取得し、それを元に新しい計算や変換を行いたい。
  2. オリジナルのデータは保持しつつ、一時的にそのコピーを操作したい。
  3. 複数のモジュールや関数で同じ配列データを共有するが、それぞれが独立してそのデータを変更できるようにしたい。

これらの状況で、もし配列の「複製」の仕組みを正しく理解していないと、意図せず元の配列を変更してしまったり、他の箇所で使われている配列に影響を与えてしまったりするリスクがあります。これが、Rubyにおける配列の複製、特にシャローコピーディープコピーの概念を深く理解するべき理由です。

この問題を理解するためには、まずRubyにおける「オブジェクト」と「変数」の関係性から見ていきましょう。


2. Rubyにおけるオブジェクトと参照の基本

Rubyでは、「全てがオブジェクトである」という思想が徹底されています。数字、文字列、配列、ハッシュ、メソッド、クラス、モジュール...これら全てがオブジェクトです。

そして、Rubyにおける変数は、オブジェクトそのものを保持しているわけではありません。変数に代入されるのは、オブジェクトがメモリ上のどこにあるかを示す「参照(Reference)」です。

a = [1, 2, 3] # 配列オブジェクトが生成され、変数aはそのオブジェクトへの参照を持つ
b = a         # 変数bは、変数aと同じオブジェクトへの参照を持つ

このコードを実行すると、abは全く同じ配列オブジェクトを指しています。これを「参照渡し」と呼びます。

p a.object_id # => 70104760867440 (例)
p b.object_id # => 70104760867440 (例)
p a == b      # => true

object_idは、そのオブジェクトがメモリ上で一意に識別されるIDです。abobject_idが同じであることから、これらが同じオブジェクトを参照していることが分かります。

この状態でbを介して配列を変更すると、どうなるでしょうか?

b << 4 # bの参照する配列に4を追加

p a # => [1, 2, 3, 4]
p b # => [1, 2, 3, 4]

ご覧の通り、bを変更しただけなのに、aの内容も変わってしまいました。これはabが同じオブジェクトを参照しているためです。

これが、配列の複製を考える上で最も基本的な、そして最も重要なポイントです。


3. シャローコピー(浅いコピー)の理解と落とし穴

シャローコピー(Shallow Copy)、または浅いコピーとは、元のオブジェクトそのものは複製するが、そのオブジェクトが参照している他のオブジェクト(要素など)は複製しないコピー方法です。

Rubyの配列において、最も一般的なシャローコピーの方法がいくつかあります。

3.1. Array#dup メソッド

dupメソッドは、レシーバーであるオブジェクトのシャローコピーを作成します。

original_array = [1, 2, 3]
copied_array   = original_array.dup

p original_array.object_id # => 70104760867440 (例)
p copied_array.object_id   # => 70104760867080 (例)
p original_array == copied_array # => true (要素は同じなので)

dupによって新しい配列オブジェクトが作成され、object_idが異なることが分かります。これにより、copied_arrayを破壊的に変更しても、original_arrayには影響を与えなくなります。

copied_array << 4

p original_array # => [1, 2, 3]
p copied_array   # => [1, 2, 3, 4]

これは期待通りの挙動です。しかし、これがシャローコピーの「落とし穴」を理解するための第一歩となります。

3.2. Array#clone メソッド

cloneメソッドもdupと同様にオブジェクトのシャローコピーを作成します。では、dupcloneは何が違うのでしょうか?

clonedupよりもより「完全な」コピーを作成します。具体的には、以下の点が異なります。

  • 特異メソッドのコピー: cloneはレシーバーに定義された特異メソッドもコピーします。dupはコピーしません。
  • フリーズ状態のコピー: オブジェクトがfreezeされていれば、cloneされたオブジェクトもfreezeされます。dupfreeze状態をコピーしません。
  • taint/untrust状態のコピー: taintuntrust状態もcloneはコピーしますが、dupはコピーしません。(Ruby 2.7以降ではtaintuntrustは非推奨・削除されていますが、過去のバージョンや互換性を考慮する際に重要でした。)

dup vs clone の比較例

class MyArray < Array
  def hello
    "Hello from MyArray!"
  end
end

original = MyArray.new([1, 2, 3])
def original.singleton_method_example; "I am a singleton method"; end
original.freeze # フリーズする

# dupでコピー
dupped = original.dup
# cloneでコピー
cloned = original.clone

puts "--- Original ---"
p original.frozen?             # => true
p original.singleton_methods   # => [:singleton_method_example]
puts original.hello            # => "Hello from MyArray!"

puts "\n--- Dupped ---"
p dupped.frozen?               # => false (dupはフリーズ状態をコピーしない)
p dupped.singleton_methods     # => [] (dupは特異メソッドをコピーしない)
# puts dupped.hello            # => "Hello from MyArray!" (これは可能、クラスメソッドなので)
dupped << 4 # フリーズされていないので変更可能
p dupped                       # => [1, 2, 3, 4]

puts "\n--- Cloned ---"
p cloned.frozen?               # => true (cloneはフリーズ状態をコピーする)
p cloned.singleton_methods     # => [:singleton_method_example] (cloneは特異メソッドをコピーする)
# puts cloned.hello            # => "Hello from MyArray!" (これは可能、クラスメソッドなので)
# cloned << 4                  # 実行時エラー: can't modify frozen Array (FrozenError)

結論として、ほとんどの場合、配列のシャローコピーにはdupを使用すれば十分です。 cloneは、オブジェクトの「状態」や「特異メソッド」も含めて忠実に複製したい、という特殊なケースで利用を検討します。

3.3. その他のシャローコピー方法

dupclone以外にも、実質的にシャローコピーとなる操作があります。

  • Array#slice (または array[0..-1]): 配列の全範囲をスライスすることで、新しい配列オブジェクトが生成されます。

    original = [1, 2, 3]
    sliced   = original[0..-1] # または original.slice(0..-1)
    p original.object_id # => 70104760867440
    p sliced.object_id   # => 70104760866940
    sliced << 4
    p original # => [1, 2, 3]
    p sliced   # => [1, 2, 3, 4]
    
  • Array#map: 配列の各要素を変換して新しい配列を生成します。変換がなければ、元の要素への参照を持つシャローコピーになります。

    original = [1, 2, 3]
    mapped   = original.map { |x| x }
    p original.object_id # => 70104760867440
    p mapped.object_id   # => 70104760866800
    mapped << 4
    p original # => [1, 2, 3]
    p mapped   # => [1, 2, 3, 4]
    
  • Array#concat (または + 演算子): 空の配列に元の配列を結合することで、新しい配列が生成されます。

    original = [1, 2, 3]
    concatenated = [] + original
    p original.object_id # => 70104760867440
    p concatenated.object_id # => 70104760866660
    concatenated << 4
    p original     # => [1, 2, 3]
    p concatenated # => [1, 2, 3, 4]
    

これらはすべて、新しい配列オブジェクトを生成し、その配列の要素として元の配列の要素への参照を格納します。

3.4. シャローコピーの「落とし穴」:ネストされたオブジェクト

さて、ここからがシャローコピーの真髄であり、多くのプログラマーがはまる「落とし穴」です。 シャローコピーは、配列の要素がオブジェクト(特に変更可能なオブジェクト、例えば別の配列やハッシュ、カスタムクラスのインスタンスなど)である場合に問題を引き起こします。

例を見てみましょう。ネストされた配列(多次元配列)の場合です。

original_nested_array = [1, [2, 3], 4]
copied_nested_array   = original_nested_array.dup

p original_nested_array.object_id # => 70104760867440
p copied_nested_array.object_id   # => 70104760866660

# ネストされた配列のobject_idを確認
p original_nested_array[1].object_id # => 70104760866380
p copied_nested_array[1].object_id   # => 70104760866380

おや? original_nested_arraycopied_nested_arrayobject_idは異なっていますが、ネストされた配列[2, 3]object_idは全く同じです。 これは、duporiginal_nested_arrayという「箱」は複製したものの、その箱の中に入っている[2, 3]という「オブジェクトそのもの」は複製せず、元のオブジェクトへの「参照」をコピーしたに過ぎないことを意味します。

この状態で、copied_nested_arrayを介してネストされた配列を変更してみましょう。

copied_nested_array[1] << 5 # copied_nested_arrayの2番目の要素(ネストされた配列)に5を追加

p original_nested_array # => [1, [2, 3, 5], 4]
p copied_nested_array   # => [1, [2, 3, 5], 4]

なんと、original_nested_arrayのネストされた配列まで変更されてしまいました!

これがシャローコピーの最も重要な注意点です。 シャローコピーは、一番外側の配列オブジェクトのみを複製し、その要素が指し示すオブジェクトは元の配列と共有されたままになります。要素が数値や文字列(これらは通常イミュータブルなため、見た目上問題になりにくい)であれば良いのですが、要素が変更可能なオブジェクト(配列、ハッシュ、カスタムクラスのインスタンスなど)である場合、意図しない副作用を引き起こす可能性があります。

この問題を解決するのが、「ディープコピー」です。


4. ディープコピー(深いコピー)の必要性と実現方法

ディープコピー(Deep Copy)、または深いコピーとは、元のオブジェクトだけでなく、そのオブジェクトが参照している全てのネストされたオブジェクトも再帰的に複製するコピー方法です。これにより、元のオブジェクトと複製されたオブジェクトは完全に独立し、どちらか一方を変更してももう一方に影響を与えることはありません。

Rubyには標準でディープコピーを行うための直接的なメソッドは提供されていません。しかし、いくつかの方法でディープコピーを実現できます。

4.1. Marshal.load(Marshal.dump(object)) を利用する方法

RubyのMarshalモジュールは、オブジェクトをバイトストリームに変換(シリアライズ)したり、バイトストリームからオブジェクトを再構築(デシリアライズ)したりする機能を提供します。この性質を利用してディープコピーを実現できます。

original_nested_array = [1, [2, 3], {a: 10, b: 20}]
deep_copied_array     = Marshal.load(Marshal.dump(original_nested_array))

p original_nested_array.object_id # => 70104760867440
p deep_copied_array.object_id     # => 70104760866080

# ネストされた配列のobject_idを確認
p original_nested_array[1].object_id # => 70104760865940
p deep_copied_array[1].object_id     # => 70104760865800 (異なる!)

# ネストされたハッシュのobject_idを確認
p original_nested_array[2].object_id # => 70104760865760
p deep_copied_array[2].object_id     # => 70104760865620 (異なる!)

Marshal.load(Marshal.dump(object))を使うと、元の配列オブジェクトはもちろん、ネストされた配列やハッシュのオブジェクトIDもすべて異なるものが生成されました。これで完全に独立したコピーが作成されたことになります。

変更を加えてみましょう。

deep_copied_array[1] << 5
deep_copied_array[2][:c] = 30

p original_nested_array # => [1, [2, 3], {:a=>10, :b=>20}]
p deep_copied_array     # => [1, [2, 3, 5], {:a=>10, :b=>20, :c=>30}]

今度はoriginal_nested_arrayは変更されず、deep_copied_arrayだけが変更されました。期待通りのディープコピーが実現できています。

Marshalを利用する際の注意点

Marshal.load(Marshal.dump(object))は非常に便利ですが、いくつかの制約があります。

  • シリアライズできないオブジェクト: ProcIOThreadなどの特定のオブジェクトはMarshalでシリアライズできません。これらのオブジェクトが配列の要素に含まれている場合、エラーが発生します。
    # Marshalでシリアライズできない例
    my_proc = -> { puts "hello" }
    arr_with_proc = [1, my_proc, 3]
    # Marshal.load(Marshal.dump(arr_with_proc)) # => TypeError: can't dump anonymous module/class
    
  • 速度: 大量のデータや非常に深いネストを持つオブジェクトに対しては、シリアライズとデシリアライズのオーバーヘッドが大きくなり、パフォーマンスに影響を与える可能性があります。
  • 互換性: Rubyのバージョンによっては、Marshalのデータ形式が変更されることがあります。異なるRubyバージョン間でMarshalされたデータをやり取りすると問題が発生する可能性があります。(ただし、同一バージョン内での利用では通常問題ありません)

4.2. Active Supportの deep_dup メソッド (Railsユーザー向け)

もしあなたがRuby on Rails環境で開発しているのであれば、Active Supportが提供する#deep_dupメソッドが非常に強力で便利です。deep_dupは、ArrayHashなどのオブジェクトに対して再帰的にディープコピーを実行します。

Active Supportのdeep_dupは、Marshalの制約の一部を克服し、より柔軟にディープコピーを実現します。

# Railsコンソールなど、Active Supportがロードされている環境で実行

# 例1: ネストされた配列
original_nested_array = [1, [2, 3], {a: 10, b: 20}]
deep_copied_array_as = original_nested_array.deep_dup

deep_copied_array_as[1] << 5
deep_copied_array_as[2][:c] = 30

p original_nested_array    # => [1, [2, 3], {:a=>10, :b=>20}]
p deep_copied_array_as     # => [1, [2, 3, 5], {:a=>10, :b=>20, :c=>30}]

# 例2: カスタムオブジェクトを含む配列
class User
  attr_accessor :name
  def initialize(name); @name = name; end
end

user1 = User.new("Alice")
original_users = [user1, User.new("Bob")]
deep_copied_users = original_users.deep_dup

deep_copied_users[0].name = "Alicia" # コピー側のユーザー名を変更

p original_users[0].name # => "Alice"
p deep_copied_users[0].name # => "Alicia"

deep_dupは、Marshalのようなシリアライズの制約が少なく、Procのようなオブジェクトも要素に含まれていなければ、デフォルトでうまく機能します。カスタムオブジェクトの場合でも、そのオブジェクトが適切にdup可能であれば、期待通りに動作します。

注意点: deep_dupはRails環境(Active Supportがロードされている環境)でのみ利用可能です。もしRailsを使用していない場合は、Marshal.load(Marshal.dump(object))を使うか、次で紹介するような自作のディープコピーメソッドを検討する必要があります。

4.3. 自作のディープコピーメソッド(カスタム実装)

特定のオブジェクトがMarshalでシリアライズできない、あるいはActive Supportが利用できない、といった特殊な要件がある場合、自分でディープコピーのロジックを実装することも可能です。これは通常、再帰的な処理になります。

# ArrayとHashにdeep_copyメソッドをモンキーパッチする例(注意して使用すること)
module DeepCopyable
  def deep_copy
    if self.is_a?(Array)
      self.map(&:deep_copy)
    elsif self.is_a?(Hash)
      Hash[self.map { |k, v| [k, v.deep_copy] }]
    else
      self.dup rescue self # dupできないオブジェクトはそのまま返す
    end
  end
end

class Object
  include DeepCopyable
end

# 使用例
original_data = [
  "string",
  123,
  [1, {x: 10, y: [1, 2]}],
  {key1: "value1", key2: [4, 5]},
  Class.new { attr_accessor :name }.new.tap { |o| o.name = "MyObject" }
]

deep_copied_data = original_data.deep_copy

# 変更を加えてみる
deep_copied_data[2][1][:y] << 3
deep_copied_data[3][:key2][0] = 999
deep_copied_data[4].name = "MyCopiedObject"

p original_data # => ["string", 123, [1, {:x=>10, :y=>[1, 2]}], {:key1=>"value1", :key2=>[4, 5]}, #<Class:0x... @name="MyObject">]
p deep_copied_data # => ["string", 123, [1, {:x=>10, :y=>[1, 2, 3]}], {:key1=>"value1", :key2=>[999, 5]}, #<Class:0x... @name="MyCopiedObject">]

p original_data[2][1][:y].object_id # => 70104760863640
p deep_copied_data[2][1][:y].object_id # => 70104760863380 (異なる)

このカスタム実装は、ArrayHashのネストを再帰的に処理し、それ以外のオブジェクトはdupしようと試みます。dupできない場合は、そのオブジェクト自体を返します(イミュータブルなオブジェクトや、dupが不必要なオブジェクトの場合)。

メリット:

  • Marshalのシリアライズ制約を受けない。
  • 特定のオブジェクトタイプに対して、よりきめ細やかなディープコピーロジックを実装できる。

デメリット:

  • 実装が複雑になる。
  • 考慮すべきオブジェクトタイプが増えるほど、コード量が増え、バグのリスクも高まる。
  • 循環参照を適切に処理しないと無限ループに陥る可能性がある。

通常、このレベルのカスタム実装が必要になるケースは稀です。多くの場合、Marshal.load(Marshal.dump(object))deep_dupで十分でしょう。


5. dup / clone / deep_dup の使い分けと判断基準

これまでの説明を踏まえ、どのメソッドを使うべきかを判断するためのフローチャートとまとめを見ていきましょう。

判断フローチャート

  1. 最も外側の配列オブジェクトだけを複製すれば良いか?

    • Yes: Array#dup を使う。
      • (もしオブジェクトの特異メソッドやフリーズ状態もコピーしたい場合は Array#clone を検討)
    • No: 次の質問へ
  2. 配列の要素に、他のオブジェクト(配列、ハッシュ、カスタムクラスのインスタンスなど)が含まれており、それらも完全に独立して複製する必要があるか?

    • Yes: ディープコピーが必要。
      • Ruby on Rails環境か?
        • Yes: Array#deep_dup を使うのが最も簡単でおすすめ。
        • No: Marshal.load(Marshal.dump(object)) を使う。
          • ただし、Proc, IO, Thread などシリアライズできないオブジェクトが含まれていないか注意。
          • これらのオブジェクトが含まれている、またはパフォーマンスがボトルネックになる場合は、カスタムのディープコピー実装を検討。

まとめ表

メソッド / 方法 コピー範囲 ネストされたオブジェクト 特異メソッド/フリーズ状態 主なユースケース 注意点
Array#dup シャローコピー(浅い) 参照をコピー コピーしない 破壊的変更から元の配列を保護したい場合。要素が数値や文字列のみの場合。 ネストされた変更可能なオブジェクトは共有される
Array#clone シャローコピー(浅い) 参照をコピー コピーする dupに加えて、オブジェクトのより忠実なコピーが必要な場合。 ネストされた変更可能なオブジェクトは共有される。フリーズされたオブジェクトは変更不可。
Marshal.load(Marshal.dump(object)) ディープコピー(深い) オブジェクトを複製 コピーする(状態として) ネストされたオブジェクトも完全に独立させたい場合。 Proc, IOなど、シリアライズできないオブジェクトはエラー。パフォーマンスの問題が生じる可能性。
Array#deep_dup (Active Support) ディープコピー(深い) オブジェクトを複製 コピーする Rails環境でディープコピーが必要な場合。 Rails環境でのみ利用可能。一部の特殊なオブジェクトは非対応の場合あり。
自作ディープコピー ディープコピー(深い) オブジェクトを複製 カスタム実装に依存 上記方法で解決できない特殊なケース。 実装が複雑。バグのリスク。循環参照の考慮が必要。

6. 実際の開発での注意点とベストプラクティス

配列の複製は、コードの安全性と予測可能性を保つ上で非常に重要です。ここでは、配列を扱う上でのベストプラクティスと一般的な注意点を紹介します。

6.1. 破壊的メソッドを意識する

Rubyのメソッドには、レシーバー(オブジェクト自身)を変更する破壊的メソッドと、レシーバーを変更せずに新しいオブジェクトを返す非破壊的メソッドがあります。破壊的メソッドは通常、メソッド名の末尾に!が付きますが、そうでないものも存在します(例: Array#<<, Array#pop, Array#shift, Array#deleteなど)。

  • 破壊的メソッドの例:
    arr = [1, 2, 3]
    arr.push(4) # => [1, 2, 3, 4] (arr自身が変更される)
    arr.sort!   # => [1, 2, 3, 4] (arr自身が変更される)
    
  • 非破壊的メソッドの例:
    arr = [1, 2, 3]
    new_arr = arr.push(4) # arrは変更されるが、new_arrは結果を保持する
    new_arr = arr.sort    # arrは変更されず、新しいソート済み配列が返る
    

もし元の配列を変更したくない場合は、これらの破壊的メソッドを呼び出す前にdupdeep_dupを使って複製を作成することが重要です。

original_data = ["apple", "banana", "orange"]
processed_data = original_data.dup # 複製を作成
processed_data.map!(&:upcase)      # 破壊的メソッドを呼び出し

p original_data  # => ["apple", "banana", "orange"] (変更なし)
p processed_data # => ["APPLE", "BANANA", "ORANGE"]

6.2. イミュータブル(不変)なデータ構造の考え方

プログラミングパラダイムの一つに、イミュータブルなデータ構造という考え方があります。これは、一度作成されたオブジェクトは決して変更されない(不変である)という原則です。変更が必要な場合は、常に新しいオブジェクトを作成して返します。

Rubyの配列はミュータブル(変更可能)ですが、イミュータブルな考え方を取り入れることで、プログラムの予測可能性を高め、意図しない副作用を減らすことができます。

# 悪い例: 配列を直接変更する関数
def add_item_to_list!(list, item)
  list << item
end

my_list = [1, 2]
add_item_to_list!(my_list, 3)
p my_list # => [1, 2, 3] # 元のリストが変更された

# 良い例: 新しい配列を返す関数 (イミュータブルなアプローチ)
def add_item_to_list(list, item)
  list + [item] # 新しい配列を返す
end

my_list = [1, 2]
new_list = add_item_to_list(my_list, 3)
p my_list    # => [1, 2] # 元のリストは変更されない
p new_list   # => [1, 2, 3]

Rubyでは、Array#freezeメソッドを使って配列を凍結することもできます。凍結された配列は変更しようとするとFrozenErrorが発生します。

frozen_array = [1, 2, 3].freeze
# frozen_array << 4 # => FrozenError: can't modify frozen Array

6.3. ドメインモデルのオブジェクト設計を考慮する

配列の要素が、単純なプリミティブ(数値、文字列)ではなく、カスタムのドメインオブジェクト(UserProductなど)である場合、そのオブジェクト自体のミュータビリティ(変更可能性)も考慮する必要があります。

例えば、Userオブジェクトがname属性を持つとして、そのnameが文字列(Rubyでは文字列はミュータブル)であれば、複製された配列のUserオブジェクトを介してnameを変更すると、元の配列のUserオブジェクトのnameも変わってしまう可能性があります。

class Person
  attr_accessor :name
  def initialize(name); @name = name; end
end

original_people = [Person.new("Alice"), Person.new("Bob")]
copied_people   = original_people.dup # シャローコピー

copied_people[0].name = "Alicia" # コピーされた配列の要素(Personオブジェクト)を変更

p original_people[0].name # => "Alicia" (元の配列の要素も変更されてしまった!)
p copied_people[0].name   # => "Alicia"

このようなケースでは、Personオブジェクト自体もディープコピーするか、Personオブジェクトがイミュータブルな設計になっていることが望ましいです。

  • Personオブジェクトもディープコピーする方法(deep_dupやカスタム実装を検討)。
  • Personクラスをイミュータブルにする(attr_readerのみにし、変更メソッドは新しいPersonオブジェクトを返すようにする)。

6.4. テストを書く

配列の複製に関する挙動は、特にネストされたオブジェクトがある場合に複雑になりがちです。意図した通りに複製が機能しているか、または意図しない副作用が発生していないかを検証するために、ユニットテストをしっかり書くことが非常に重要です。

  • シャローコピーの場合: object_idを比較して、外側の配列は異なるオブジェクトだが、内側のオブジェクトは同じobject_idであることを確認するテスト。
  • ディープコピーの場合: すべてのオブジェクトのobject_idが異なることを確認するテスト。

テストを通じて、あなたのコードが配列の複製に関して期待通りの振る舞いをしていることを確認しましょう。


7. よくある質問 (FAQ)

Q1: 文字列や数値の配列ならdupで十分ですか?

A1: はい、ほとんどの場合dupで十分です。

Rubyの数値(Integer, Floatなど)はイミュータブルなオブジェクトです。また、文字列(String)はミュータブルなオブジェクトですが、配列の要素として文字列が格納されている場合、dupでシャローコピーを作成した後にその文字列自体を変更すると、元の配列の文字列も変更されてしまいます。

original_strings = ["hello", "world"]
copied_strings = original_strings.dup

copied_strings[0].upcase! # "hello"を破壊的に大文字化

p original_strings # => ["HELLO", "world"] # 元の配列の文字列も変更された!
p copied_strings   # => ["HELLO", "world"]

しかし、通常、配列の要素の文字列を変更する際は、新しい文字列を生成して要素を置き換えることが多いでしょう。

original_strings = ["hello", "world"]
copied_strings = original_strings.dup

copied_strings[0] = copied_strings[0].upcase # 新しい文字列オブジェクトを生成し、要素を置き換え

p original_strings # => ["hello", "world"] # 元の配列は変更されない
p copied_strings   # => ["HELLO", "world"]

この「新しい文字列を生成して要素を置き換える」操作であれば、dupで問題ありません。文字列自体を破壊的に変更し、その変更が元の配列にも影響するのを避けたい場合は、deep_dupを検討するか、文字列操作の際に新しい文字列を生成する非破壊的メソッド(例: upcase)を使うようにしましょう。

Q2: Array#mapで複製するのはシャローコピーですか?ディープコピーですか?

A2: Array#mapは、基本的に「シャローコピー」の挙動を示します。

mapは各要素を処理した結果を新しい配列として返します。もしmapブロック内で要素をそのまま返すだけなら、新しい配列に元の要素への参照が格納されるため、シャローコピーになります。

original = [[1], [2], [3]]
mapped   = original.map { |sub_arr| sub_arr } # 要素をそのまま返す

p original[0].object_id # => 70104760863060
p mapped[0].object_id   # => 70104760863060 (同じ参照)

mapped[0] << 4
p original # => [[1, 4], [2], [3]] # 元の配列も変更された

もしmapを使ってディープコピーのようなことをしたい場合は、ブロック内で各要素を明示的にディープコピーする必要があります。

original = [[1], [2], [3]]
deep_mapped = original.map { |sub_arr| sub_arr.dup } # 各サブ配列をdup

p original[0].object_id  # => 70104760863060
p deep_mapped[0].object_id # => 70104760862920 (異なる参照)

deep_mapped[0] << 4
p original  # => [[1], [2], [3]] # 元の配列は変更されない
p deep_mapped # => [[1, 4], [2], [3]]

しかし、これはネストが深い場合には対応できません。より汎用的なディープコピーが必要な場合は、前述のMarshaldeep_dupを使用してください。


8. まとめ

Rubyにおける配列の複製は、単なる表面的なコピーではなく、その裏にあるオブジェクトと参照の仕組みを理解することが不可欠です。

  • 参照渡し: 変数はオブジェクトそのものではなく、その参照を保持している。
  • シャローコピー (dup, clone): 一番外側の配列オブジェクトのみを複製し、要素は元の配列と参照を共有する。ネストされた変更可能なオブジェクトがある場合に落とし穴となる。
  • ディープコピー (Marshal.load(Marshal.dump(object)), deep_dup): ネストされたオブジェクトも含め、すべてを再帰的に複製し、完全に独立したコピーを作成する。
  • 使い分け: 最も外側の配列のみ保護したいならdup。ネストされたオブジェクトも完全に独立させたいならdeep_dup(Rails)またはMarshal.load(Marshal.dump(...))

これらの知識をしっかりと身につけることで、あなたはRubyの配列操作における多くのバグを回避し、より堅牢で予測可能なコードを書けるようになるでしょう。

今回の記事が、あなたのRubyプログラミングの一助となれば幸いです。もしこの記事が役に立ったと感じたら、ぜひ他のプログラマーにも共有してください!

\ この記事をシェア/
この記事を書いた人
pekemalu
I love codes. I also love prompts (spells). But I get a lot of complaints (errors). I want to be loved by both of you as soon as possible.
Image