はじめに
記事を見ていただいて、ありがとうございます!
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でシャローコピーをする場合は、以下の組み込みメソッドを使います。
シャローコピーをする組み込みのメソッドはdupとcloneがあります。
コードの実例は、シャローコピー説明時の例を参照してください。
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のコピーで、特にシャローコピーは副作用があるので気をつけて使う必要があります。
シャローコピーの使い時について明確にこれだと提示できないのですが、メモリの使用可能領域が少なく、メモリの使用量をできるだけ抑えたい時などでしょうか。
このような条件の場合はシャローコピーを使った方が良いということがありましたら、ぜひコメントで教えてください。
いずれにせよ、シャローコピーでなければならない条件は多くはないと思うので、ディープコピーを積極的に使うのが良いかなと思います。
ここまで記事を見ていただいて、ありがとうございました!!
コメント