前回までの内容を簡単にまとめると、
- =でidが変えられる。idが変わると参照する値ももちろん変わる。
- 破壊的な関数はidを変えずに参照する値を変える。
このように変数の値の変更手段について見てきた。
しかし実際の実装で困るのは変更方法よりもむしろ変更しない方法だろう。
破壊的メソッドを使わないことは出来るかもしれないが、=を使わないでコードを書くのは難しいと思う。
=を使う限り、常に気をつけねばならないので、今回はその話をする。
ruby 破壊の話シリーズ |
---|
ruby 破壊の話①「=」のふるまい |
ruby 破壊の話②メソッド編 |
ruby 破壊の話③関数編 |
ruby 破壊の話④破壊的なメソッド編 |
ruby 破壊の話⑤dup編 |
簡単な例
まずは問題提起から。実際に困るのはこういう場合である。
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関数は例えば面接の合否を返す関数だとして、対象はtestさん(a)とtさん(b)だとする。
名前を書いてjudgeすると、trueかnilが返ってくると。
testさんが合格でtさんが不合格なのですが、返り値に注目。pointが格納されて帰ってきています。
なんだ名前の長さのみで評価されるのかよって話になってしまいます。
だいぶ意味不明な例になりましたが、言いたいことは伝わったかなと思います。
judge関数自体は合否のみを返しているつもりですが、もらった引数自体を書き換えてしまっているんですよね。
今回は追加だったので情報漏洩ですが、削除してしまうことももちろん出来てしまいます。
破壊的と聞くと削除だけを指すイメージもありますが、削除はもとより追加、変更など、少しでも変化がある場合全般について破壊と呼びます。
破壊的な関数を使わない/作らない
先のコードの問題点はただ一つ。judge関数が破壊的である引数を破壊してしまうことです。
もしも情報漏洩が正しい(望んでいる)仕様ならば、動きとしては期待通りです。
しかし、一目でjudge関数が破壊的である引数を破壊してしまうことがわかる様に"!"をつけ、judge!関数にするべきと言えます。
これならaやbを渡す段で、どんな状態で(名前が消えたり)帰ってきてもある程度は覚悟できているはず。
ただそもそも破壊的な引数を破壊してしまう関数自体危険性が高いので、実装は必要最低限にとどめるべきと考えます。
もっと言うと、実際の仕様はさておき、どう書いたら破壊的で、どうすれば破壊的でなくなるのかを判断できるようになることは重要だと思います。
関数の基本
では実際にidを確認して、どのように書けばよいのか、まずは仕様を理解してみます。
irb(main):001:0> def get_id(obj)
irb(main):002:1> return obj.__id__
irb(main):003:1> end
=> :get_id
irb(main):004:0> a='abc'
=> "abc"
irb(main):005:0> a.__id__
=> 47110191884680
irb(main):006:0> b=:abc
=> :abc
irb(main):007:0> b.__id__
=> 1162588
irb(main):008:0> get_id(a) #aと同じid
=> 47110191884680
irb(main):009:0> get_id(b) #bと同じid
=> 1162588
irb(main):010:0> a
=> "abc"
irb(main):011:0>
get_id関数に引数として渡してみましたが、中で評価してもやはりidは同じようですね。
シンボルはidも変わらなさそうでしたが、文字列も変わらないみたいです。
実はrubyの関数が引数として受け取るのは値ではなくidのようで、これこそがrubyの仕様っぽいですね。
irb(main):001:0> def reverser(obj)
irb(main):002:1> p obj.__id__
irb(main):003:1> obj.reverse!
irb(main):004:1> p obj.__id__
irb(main):005:1> obj
irb(main):006:1> end
=> :reverser
irb(main):007:0> a='abc'
=> "abc"
irb(main):008:0> reverser(a)
47215576028320
47215576028320
=> "cba"
irb(main):009:0> a
=> "cba"
うーんしっかり破壊されている。(reverser関数も!を付けるべきですね。)
関数に引数として渡しても同じidだから、破壊的メソッドを使うと破壊されちゃうわけです。
これだけ聞くと困っちゃうんですよね。
破壊的メソッドはともかく、=も関数の先で破壊しないで運用するのって難しい。。。どうやったら関係を断ち切れるの。。。
という話が次の記事に続きます。