前回までで、破壊的メソッドを無意識に作らない方法について書いてきました。
その一つとしてdupを使った方法が知られていますが、どうしてdupを使うと回避できるのか、dupを使っても回避できない場合があること、完全に断ち切る方法について書いていこうと思います。
ruby 破壊の話シリーズ |
---|
ruby 破壊の話①「=」のふるまい |
ruby 破壊の話②メソッド編 |
ruby 破壊の話③関数編 |
ruby 破壊の話④破壊的なメソッド編 |
ruby 破壊の話⑤dup編 |
dupの動き
まずはdupとはどんな動きをするのかを検証します。
irb(main):001:0> a=['a','b','c']
=> ["a", "b", "c"]
irb(main):002:0> b=['a','b','c']
=> ["a", "b", "c"]
irb(main):003:0> c=a
=> ["a", "b", "c"]
irb(main):004:0> d=a.dup
=> ["a", "b", "c"]
irb(main):005:0> a.__id__
=> 47190903357720
irb(main):006:0> b.__id__
=> 47190903332260
irb(main):007:0> c.__id__
=> 47190903357720
irb(main):008:0> d.__id__
=> 47190903309860
abcdどれも中身は同じ配列ですが、idはaとcが同じだけで、全部で3つの別々のオブジェクトが生成されていることがわかると思います。
aとcが同じなのは003で参照先を揃えたからですね。
今回の主役dupは004で使われています。
dupはコピーした新しいオブジェクトを参照するので、全部で3つになったわけです。
なのでaをもとに生成されたものの、オブジェクト自体は異なるので、好き勝手に操作してもaに影響を与えないわけです。
irb(main):011:0> a<<'d'
=> ["a", "b", "c", "d"]
irb(main):012:0> a
=> ["a", "b", "c", "d"]
irb(main):013:0> c
=> ["a", "b", "c", "d"]
irb(main):014:0> d
=> ["a", "b", "c"]
irb(main):015:0> d<<'ee'
=> ["a", "b", "c", "ee"]
irb(main):016:0> a
=> ["a", "b", "c", "d"]
irb(main):017:0> d.pop
=> "ee"
irb(main):018:0> d.pop
=> "c"
irb(main):019:0> d
=> ["a", "b"]
irb(main):020:0> a
=> ["a", "b", "c", "d"]
新しいオブジェクトなのでこのように好き勝手に扱えますが、今回書きたいのは、完全な複製ではないという点です。
なので好き勝手に扱えるかというと実はそうでもありません。
むしろ中途半端な複製なので、私はあまり使いたくありません。
irb(main):001:0> a=["abc","def","ghi"]
=> ["abc", "def", "ghi"]
irb(main):002:0> b=a.dup
=> ["abc", "def", "ghi"]
irb(main):003:0> a
=> ["abc", "def", "ghi"]
irb(main):004:0> b
=> ["abc", "def", "ghi"]
irb(main):005:0> b[0].reverse!
=> "cba"
irb(main):006:0> b
=> ["cba", "def", "ghi"]
irb(main):007:0> a #dupしたはずなのにaも変わっている
=> ["cba", "def", "ghi"]
このように破壊的メソッドの影響がコピー前の配列にも及んでしまいました。
破壊的なメソッドで起こるということは、=でも当然起こります。
irb(main):001:0> a=["abc","def","ghi"]
=> ["abc", "def", "ghi"]
irb(main):002:0> b=a
=> ["abc", "def", "ghi"]
irb(main):003:0> obj1=[a,b]
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]]
irb(main):004:0> obj2=obj1.dup
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]]
irb(main):005:0> obj2[0][0]="aaa"#obj2の最初の要素だけ書き換えたい
=> "aaa"
irb(main):006:0> obj2
=> [["aaa", "def", "ghi"], ["aaa", "def", "ghi"]]
irb(main):007:0> obj1 #obj1の最初の要素も書き換えられてしまった
=> [["aaa", "def", "ghi"], ["aaa", "def", "ghi"]]
irb(main):008:0> obj2[0].reverse!#obj2の最初の配列すべてを反転させると
=> ["ghi", "def", "aaa"]
irb(main):009:0> obj2
=> [["ghi", "def", "aaa"], ["ghi", "def", "aaa"]]
irb(main):010:0> obj1
=> [["ghi", "def", "aaa"], ["ghi", "def", "aaa"]] #もれなく全部反転されます。
irb(main):011:0> obj1[0].__id__
=> 46991556482860
irb(main):012:0> obj1[1].__id__
=> 46991556482860
irb(main):013:0> obj2[0].__id__
=> 46991556482860
irb(main):014:0> obj2[1].__id__
=> 46991556482860
irb(main):015:0> #みんな同じものを見ていたらそりゃそう
何が起こっているか
察しの良い方はお気づきかもしれませんが、関数に渡したときと似たような事象が起こっています。
ローカル変数という概念はここではありませんが、dupはローカル変数を作るときと同じようなふるまいをしています。
ここではobj2というオブジェクトをobj1の複製としているので、obj2を変更する限りは複製前に影響を与えません。
しかしその先、例えばobj2[0]やobj[0][0]は、全く同じものを指しているのでもろに影響が出るわけですね。
解決策
実際ここまでやる必要があるかは場合によりますが、少なくとも完全に断ち切る方法位マスターしたいですよね・・・??少なくとも僕はしたいですね。
どこか壊さないか恐れながら実装したくないので。
以下の方法で複製元との関係をすべて断ち切った完全な複製が作れます。
irb(main):001:0> a=["abc","def","ghi"]
=> ["abc", "def", "ghi"]
irb(main):002:0> b=a
=> ["abc", "def", "ghi"]
irb(main):003:0> obj1=[a,b]
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]]
irb(main):004:0> obj3 = Marshal.load(Marshal.dump(obj1)) #obj1の完全な複製としてobj3を作成する
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]]
irb(main):005:0> obj1
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]]
irb(main):006:0> obj3[0][0].reverse!
=> "cba"
irb(main):007:0> obj3[0].reverse!
=> ["ghi", "def", "cba"]
irb(main):008:0> obj3
=> [["ghi", "def", "cba"], ["ghi", "def", "cba"]]
irb(main):009:0> obj1
=> [["abc", "def", "ghi"], ["abc", "def", "ghi"]] #無傷
irb(main):010:0> Marshal.dump(obj1) => "\x04\b[\a[\bI\"\babc\x06:\x06ETI\"\bdef\x06;\x00TI\"\bghi\x06;\x00T@\x06"
irb(main):012:0> obj3[0].__id__ => 46956196664760
irb(main):013:0> obj3[1].__id__ => 46956196664760
irb(main):014:0> obj1[1].__id__ => 46956196746500
仕組みは簡単で、複製の過程で一回単なる文字列になってます。
そこからもう一度オブジェクトを作り直すので、完全なる独立した複製が出来るわけです。
面白いのが、obj3[0]とobj3[1]の関係性は保持されているところ。
マーシャルデータ自体はオブジェクト内の関係性を保持する仕様の様です。
なので断ち切られる関係性はあくまで複製前と複製後の関係だけ。素晴らしい。