[Ruby/Ruby on Rails] ArrayやHashのコピー

Ruby

はじめに

記事を見ていただいて、ありがとうございます!

Webエンジニアをしているsannoと申します。

開発をしてゆく上でArrayやHashの操作は必要不可欠だと思います。

ArrayやHashで別の変数にコピーしてから色々と操作したいということも、よくあると思います。

ただ、そのコピー操作がシャローコピーなのかディープコピーなのか意識しておかないと、思わぬ副作用でバグを発生させてしまうかもしれません。

ですので、今回はRubyやRuby on RailsでのArrayやHashのコピー操作について解説したいと思います。

良かったら見ていってください。

ArrayやHashのコピーメソッド

手取り早くコピーメソッドだけを確認したいという方は以下を参照してみてください。

環境

$ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [aarch64-linux]

$ bundle exec rails --version
Rails 7.0.8

ArrayやHashのコピー

シャローコピー

シャローコピーとは引用させていただくと、以下の意味です。

シャローコピーとは、配列オブジェクトなどのデータ構造を複製する際、参照のみをコピーして実体の複製は作らない方式。

https://e-words.jp/w/%E3%82%B7%E3%83%A3%E3%83%AD%E3%83%BC%E3%82%B3%E3%83%94%E3%83%BC.html

シャローコピーは参照をしているコピーです。

「参照をしている」と表現をしてしまいましたが、このあたりことについては正確な解説ができる自信がないので、以下の記事などを参考にしてみてください。

Rubyの実例でシャローコピーについて見てみましょう。

(シャローコピーのメソッドについてはのちほど解説します)

array_from = ['value1', 'value2', 'value3']
array_to = array_from.dup

array_to
# => ["value1", "value2", "value3"]

array_from[0].object_id
# => 654460

array_to[0].object_id
# => 654460

array_to[0].upcase!

array_from
# => ["VALUE1", "value2", "value3"]
# array_to[0]と参照関係なので影響する

array_to
# => ["VALUE1", "value2", "value3"]
hash_from = {key1: 'value1', key2: 'value2', key3: {key3_key1: 'value3'}}
hash_to = hash_from.dup

hash_from[:key1].object_id
# => 9885780

hash_to[:key1].object_id
# => 9885780

hash_to[:key1].upcase!

hash_from
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}
# hash_to[:key1]と参照関係なので影響する

hash_to
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}

hash_from[:key3][:key3_key1].object_id
# => 10101120

hash_to[:key3][:key3_key1].object_id
# => 10101120

hash_to[:key3][:key3_key1].upcase!

hash_from
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"VALUE3"}}
# hash_to[:key3][:key3_key1]と参照関係なので影響する

hash_to
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"VALUE3"}}

ディープコピーはコピー元と同じobject_idになる、つまり参照をしていると言えます。

参照しているので、 コピー先のarray_toを変更するとコピー元のarray_fromも変わってしまいます。

これはシャローコピーの副作用と言えるでしょう。

シャローコピーの組み込みメソッド

Rubyの組み込みメソッド

Rubyでシャローコピーをする場合は、以下の組み込みメソッドを使います。

シャローコピーをする組み込みのメソッドはdupcloneがあります。

コードの実例は、シャローコピー説明時の例を参照してください。

dupもcloneも同じように参照を持ったコピーになります。

では、dupとcloneの違いはどこにあるのでしょうか。

dupとcloneの違いは以下のように説明されています。

dup はオブジェクトの内容をコピーし、 clone はそれに加えて freeze, 特異メソッドなどの情報も含めた完全な複製を作成します。

https://docs.ruby-lang.org/ja/latest/method/Object/i/clone.html

dupはfreezeや特異メソッドの情報をコピーしないですが、cloneはそれらの情報をコピーするということですね。

コードで示すと以下のような違いになります。

copy_from = ['value1', 'value2', 'value3']

def copy_from.hello_world
  puts 'hello world'
end

copy_from.freeze

copy_dup = copy_from.dup

copy_dup.hello_word
# `<main>': undefined method `hello_word' for ["value1", "value2", "value3"]:Array (NoMethodError)

copy_dup.frozen?
# => false

copy_clone = copy_from.clone

copy_clone.hello_world
# => hello world

copy_clone.frozen?
# => true

ディープコピー

ディープコピーとは引用させていただくと、以下の意味です。

ディープコピーとは、配列オブジェクトなどのデータ構造を複製する際、同じ構造の実体を新たに作成して対応するデータを写し取る方式。

https://e-words.jp/w/%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%82%B3%E3%83%94%E3%83%BC.html

ディープコピーはシャローコピーとは反対に参照をしないコピーです。

コードで示すと以下のようになります。

(ディープコピーのメソッドについてはのちほど解説します)

array_from = ['value1', 'value2', 'value3']
array_to = Marshal.load(Marshal.dump(array_from))

array_to
# => ["value1", "value2", "value3"]

array_from[0].object_id
# => 6725520

array_to[0].object_id
# => 6768980

array_to[0].upcase!

array_from
# => ["value1", "value2", "value3"]
# array_to[0]の参照ではないので影響しない

array_to
# => ["VALUE1", "value2", "value3"]
hash_from = {key1: 'value1', key2: 'value2', key3: {key3_key1: 'value3'}}
hash_to = Marshal.load(Marshal.dump(hash_from))

hash_to
# => {:key1=>"value1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}

hash_from[:key1].object_id
# => 8832820

hash_to[:key1].object_id
# => 8787740

hash_to[:key1].upcase!

hash_from
# => {:key1=>"value1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}
# hash_to[:key1]の参照ではないので影響しない

hash_to
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}

hash_from[:key3][:key3_key1].object_id
# => 9013340

hash_to[:key3][:key3_key1].object_id
# => 9047140

hash_to[:key3][:key3_key1].upcase!

hash_from
# => {:key1=>"value1", :key2=>"value2", :key3=>{:key3_key1=>"value3"}}
# hash_to[:key3][:key3_key1]の参照ではないので影響しない

hash_to
# => {:key1=>"VALUE1", :key2=>"value2", :key3=>{:key3_key1=>"VALUE3"}}

ディープコピーはコピー元と異なるobject_idになる、つまり参照をせずに同じ構造の実体をコピーしていると言えます。

参照していないので、 コピー先のarray_toを変更してもコピー元のarray_fromに影響しません。

ディープコピーはシャローコピーであった、コピー元に影響を与える副作用がありません。

ディープコピーの組み込みメソッド

Rubyの組み込みメソッド

Rubyでディープコピーをする場合は、以下の組み込みメソッドを使います。

ディープコピーをするためには、Marshar.dumpでマーシャルデータを生成し、そのマーシャルデータからMarshar.loadで元のObjectを生成することでできます。

コードの実例は、ディープコピー説明時の例を参照してください。

Ruby on Railsの組み込みメソッド

Rubyでディープコピーををしたい場合には、Marshar.dumpとMarshar.loadの2段階の操作が必要でした。

2段階の操作は少し手間に感じるかもしれません。

もし、Ruby on Railsも使っている環境でしたら、一つのメソッドでディープコピーが完結する以下の組み込みメソッドがあります。

deep_dupはRuby on Railsの組み込みメソッドで、これ一つでディープコピーを完結できます。

コードの実例は以下のようになります。

array_from = ['value1', 'value2', 'value3']
array_to = array_from.deep_dup

array_to
# => ["value1", "value2", "value3"]

array_from[0].object_id
# => 149600

array_to[0].object_id
# => 157780

array_to[0].upcase!

array_from
# => ["value1", "value2", "value3"]
# array_to[0]の参照ではないので影響しない

array_to
# => ["VALUE1", "value2", "value3"]

Marshar.dumpとMarshar.loadの処理がdeep_dupに変わっただけですね。

おわりに

ArrayやHashのコピーで、特にシャローコピーは副作用があるので気をつけて使う必要があります。

シャローコピーの使い時について明確にこれだと提示できないのですが、メモリの使用可能領域が少なく、メモリの使用量をできるだけ抑えたい時などでしょうか。

このような条件の場合はシャローコピーを使った方が良いということがありましたら、ぜひコメントで教えてください。

いずれにせよ、シャローコピーでなければならない条件は多くはないと思うので、ディープコピーを積極的に使うのが良いかなと思います。

ここまで記事を見ていただいて、ありがとうございました!!

コメント

タイトルとURLをコピーしました