普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
deep_dup
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
RailsガイドのActive Supportコア拡張機能の 2.4 deep_dupの項目を見てみます
deep_dupメソッドは、与えられたオブジェクトの「ディープコピー」を返します。
Rubyは通常の場合、他のオブジェクトを含むオブジェクトをdupしても、他のオブジェクトについては複製しません。
このようなコピーは「浅いコピー (shallow copy)」と呼ばれます。たとえば、以下のように文字列を含む配列があるとします。array = ['string']
duplicate = array.dup
duplicate.push 'another-string'
# このオブジェクトは複製されたので、複製された方にだけ要素が追加された
array # => ['string']
duplicate # => ['string', 'another-string']
duplicate.first.gsub!('string', 'foo')# 1つ目の要素は複製されていないので、一方を変更するとどちらの配列も変更される
array # => ['foo']
duplicate # => ['foo', 'another-string']上で見たとおり、Arrayのインスタンスを複製して別のオブジェクトができたことにより、一方を変更しても他方は変更されないようになりました。
ただし、配列は複製されましたが、配列の要素はそうではありません。dupメソッドはディープコピーを行わないので、配列の中にある文字列は複製後も同一オブジェクトのままです。
オブジェクトをディープコピーする必要がある場合はdeep_dupをお使いください。例:array = ['string']
duplicate = array.deep_dup
duplicate.first.gsub!('string', 'foo')
array # => ['string']
duplicate # => ['foo']オブジェクトが複製不可能な場合、deep_dupは単にそのオブジェクトを返します。
number = 1
duplicate = number.deep_dup
number.object_id == duplicate.object_id # => true
オブジェクトをコピーしたいが、別物として利用したい場合に利用するメソッドのようです
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能なので、activesupportディレクトリのlib配下で def deep_dup
を探してみます
2. 該当箇所が3箇所だったので、それぞれみてみます
activesupport > lib > active_support > core_ext > object > deep_dup.rb
Object.deep_dup
class Object # Returns a deep copy of object if it's duplicable. If it's # not duplicable, returns +self+. # # object = Object.new # dup = object.deep_dup # dup.instance_variable_set(:@a, 1) # # object.instance_variable_defined?(:@a) # => false # dup.instance_variable_defined?(:@a) # => true def deep_dup duplicable? ? dup : self end end
まずduplicable?
かどうか判定しているようです
※ duplicable?のソースコードリーディングについてはこちら
shitake4.hatenablog.com
trueの場合は dup
が実行されるようです
Rubyリファレンスのclone,dupで挙動を調べてみると
cloneメソッドとdupメソッドは、レシーバのオブジェクトのコピーを作成して返します。オブジェクトのコピーとは、同じ内容を持つ別のオブジェクトです。具体的には、元のオブジェクトと同じクラスの新しいオブジェクトで、元のオブジェクトのインスタンス変数を新しいオブジェクトにコピーしたものです。
中略
浅いコピー
cloneとdupは「浅いコピー」を作ることに注意してください。
上記のCatクラスの例では、@nameがoriginalからcopiedにコピーされますが、@nameが指しているオブジェクト(文字列)は同じものです。
copiedの@nameに対して破壊的なメソッド(レシーバ自身を変更するメソッド)を呼び出すと、originalの@nameが指している文字列も変更されます。
Objectに対するdeep_dupは浅いコピーになるようです
Rubyリファレンスのclone,dupに記述している例をrails consoleで確認してみると
originalのnameも変わっています
Array.deep_dup
class Array # Returns a deep copy of array. # # array = [1, [2, 3]] # dup = array.deep_dup # dup[1][2] = 4 # # array[1][2] # => nil # dup[1][2] # => 4 def deep_dup map(&:deep_dup) end end
レシーバに対してmapしています self.map(&:deep_dup)
と同一です
(&:deep_dup)
の&:は配列の各要素に対して deep_dup
を実行しています
例えば、
[1, 2, 3].map(&:to_s) => ["1", "2", "3"]
という感じです ※詳しい解説は@kasei-san氏のこちらの記事がわかりやすいかと思います
qiita.com
ということで、配列の各要素に対してdeep_dup
をおこなっています
Hash.deep_dup
class Hash # Returns a deep copy of hash. # # hash = { a: { b: 'b' } } # dup = hash.deep_dup # dup[:a][:c] = 'c' # # hash[:a][:c] # => nil # dup[:a][:c] # => "c" def deep_dup hash = dup each_pair do |key, value| if key.frozen? && ::String === key hash[key] = value.deep_dup else hash.delete(key) hash[key.deep_dup] = value.deep_dup end end hash end end
まず浅いコピーでレシーバをhashに入れています
each_pair
を調べてみると
eachメソッドは、ハッシュの要素(キーと値)の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数にはキーkeyと値valが入ります。each_pairメソッドは、eachの別名です。
引用:Rubyリファレンス:each, each_pair (Hash)
ということなので、レシーバのpairのkey,valueを取り出しています
次に
if key.frozen? && ::String === key hash[key] = value.deep_dup
を見ていきます
まずfrozen?
を調べてみると
frozen?メソッドは、オブジェクトが凍結状態ならtrueを、そうでなければfalseを返します。オブジェクトを凍結状態にするには、freezeメソッドを使います。
引用:Rubyリファレンス:frozen?
更に freeze
を調べてみると
freezeメソッドは、オブジェクトを凍結、つまり変更不可にします。凍結状態のオブジェクトを変更しようとすると、Ruby 1.8では例外TypeErrorが、Ruby 1.9では例外RuntimeErrorが発生します。
凍結状態を調べるには、frozen?メソッドを使います。凍結状態を元に戻すメソッドはありません。
標準クラスのオブジェクトでは、凍結したあとで破壊的なメソッド(レシーバ自身を変更するメソッド)を呼び出すと例外が発生します。
引用:Rubyリファレンス:freeze
freezeされた状態 = 破壊的変更が出来なくなっているということのようです
つまり if key.frozen? && ::String === key
とは破壊的変更が出来ないStringクラスのキーの場合、valueのみをdeep_dupしています
そして
else hash.delete(key) hash[key.deep_dup] = value.deep_dup end
はkeyが破壊的変更が可能な値の為、key,valueともにdeep_dupしています
これでコピー元のkeyに影響が無いようにしています
読んでみて
Objectでdeep_dupする場合は浅いコピーの為、気をつけなければいけないと思いました
コードを読んでみてわかることもあるので、プロジェクトでライブラリなどを導入する場合はちゃんと実装まで理解しないとバグを発生させる可能性も出てきてしまうので気をつけたいです