前回までの内容を簡単にまとめると、
- =や破壊的メソッドはデータの中身を壊す危険がある
- 破壊的関数かどうか見極められると良い
- 関数の中で破壊的メソッドを使うと破壊的になる
破壊的なメソッドの判別として、一番ハマりやすい破壊的=についての話を扱う。
ruby 破壊の話シリーズ |
---|
ruby 破壊の話①「=」のふるまい |
ruby 破壊の話②メソッド編 |
ruby 破壊の話③関数編 |
ruby 破壊の話④破壊的なメソッド編 |
ruby 破壊の話⑤dup編 |
単に=を使っても破壊されない
関数に引数として渡しても同じidだから、どうやったら関係を断ち切れるの。。。と思っていましたが、そういう心配はありませんでした。
irb(main):001:0> def overWrite(obj)
irb(main):002:1> p obj.__id__
irb(main):003:1> obj='over'
irb(main):004:1> return obj.__id__
irb(main):005:1> end
=> :overWrite
irb(main):006:0> a='abc'
=> "abc"
irb(main):007:0> b=:abc
=> :abc
irb(main):008:0> a.__id__
=> 47211306018060
irb(main):009:0> b.__id__
=> 1162588
irb(main):010:0> overWrite(a)
47211306018060 #書き換え前のidは同じ
=> 47211305688140 #書き換え後は異なる
irb(main):011:0> a #書き換えられていない
=> "abc"
irb(main):012:0> a.__id__ #idも書き換えられてない
=> 47211306018060
irb(main):013:0> overWrite(b)
1162588 #書き換え前のidは同じ
=> 47211305656840 #書き換え後は異なる
irb(main):014:0> b.__id__ #書き換えられていない
=> 1162588
irb(main):015:0>
このoverWrite関数は破壊的に作ったつもりでしたが、実は破壊的ではなかったようです。
=でidを書き換えているのは前に書いた通りなのですが、実は関数のスコープの問題で渡したaやbは書き換えを逃れています。
書き換えているobjは関数overWriter内でのみ有効なので、idを書き換えているのも、関数内でのみ有効なローカル変数objのidが書き換えられているだけなんですよね。
よくよく考えるとプログラミング言語ってそうあるべきだよなって一安心。
結局=を使っても関係は断ち切れているから大丈夫なのね。安心安心。
とは言い切れないので安心もできない
ここまで述べたことが間違っているわけではありませんが、ちょっと拡大解釈するとすぐに落とし穴にはまってしまいます。
前回の問題のコードを再掲します。
irb(main):001:0> def judge(hoge)
irb(main):002:1> hoge[:point]=hoge[:name].length()
irb(main):003:1> return hoge[:point]>2 ? true : nil
irb(main):004:1> end
=> :judge
irb(main):005:0> a={:name=>'test'}
=> {:name=>"test"}
irb(main):006:0> b={:name=>'t'}
=> {:name=>"t"}
irb(main):007:0> a
=> {:name=>"test"}
irb(main):008:0> b
=> {:name=>"t"}
irb(main):009:0> judge(a)
=> true
irb(main):010:0> judge(b)
=> nil
irb(main):011:0> a
=> {:name=>"test", :point=>4}
irb(main):012:0> b
=> {:name=>"t", :point=>1}
judge関数は破壊的という話をしましたが、002の=が実は破壊的な=なんですよね。
上で述べたローカル変数objと同じように、引数として受け取ったhogeも確かにローカル変数です。
しかしローカル変数なのはhogeのみです。
例えばhoge[0]やhoge[:name]はローカル変数ではありません。
hogeとhoge[0]は違うオブジェクトですからね。ここがハマりやすいところです。
hogeが参照している値は引数として与えられたものと同じです。
hoge自体の参照先を変える「hoge=」という=はローカル変数を変更するものなので破壊的ではないですが、hogeの[:point]が参照している先はローカル変数ではないので、hoge[:point]=という=は破壊的な=になってしまうんですよね。
irb(main):001:0> def overZense(obj)
irb(main):002:1> obj[:name]='千' #名前は変えてしまう
irb(main):003:1> obj={:name=>"千",:mind =>"社畜"} #心まで全部変える
irb(main):004:1> return obj
irb(main):005:1> end
=> :overZense
irb(main):006:0> a={:name=> '千尋'}
=> {:name=>"千尋"}
irb(main):007:0> overZense(a)
=> {:name=>"千", :mind=>"社畜"}
irb(main):008:0> a #名前は変えられたけど心までは変えられない
=> {:name=>"千"}
主にArrayやHashで出てくる例ですが、条件さえそろえば文字列とかその他mutableなオブジェクト全般で現れる例です。
よくわからないまま適当に=でつないでも、ひたすらにエイリアスなので意味がないです。
irb(main):001:0> def test(obj)
irb(main):002:1> dummy=obj
irb(main):003:1> dummy[:name]='千'
irb(main):004:1> end
=> :test
irb(main):005:0> a={:name=>'宮崎'}
=> {:name=>"宮崎"}
irb(main):006:0> test(a)
=> "千"
irb(main):007:0> a
=> {:name=>"千"}
やるなら「ローカル変数=」の形にする必要があります
irb(main):001:0> def test(obj)
irb(main):002:1> dummy_name=obj[:name]
irb(main):003:1> dummy_name=dummy_name+'千'
irb(main):004:1> p dummy_name
irb(main):005:1> end
=> :test
irb(main):006:0> a={:name=>'宮崎'}
=> {:name=>"宮崎"}
irb(main):007:0> test(a)
"宮崎千"
=> "宮崎千"
irb(main):008:0> a
=> {:name=>"宮崎"}
あまり直観的でないふるまいですが、ロジックがわかればなるほど納得の仕様でした。
まとめ
破壊的なメソッドは、以下の場合に作られる。
- 破壊的メソッドを使った場合
-
{ローカル変数}=
以外の形で=を使う場合
これを回避する方法として、dupを使った方法がありますが、これも盲目的に使っては意味がないことが分かったので、最後にdup編を用意しました。