前置き
Ruby でプログラミングしてて、ついついはまってしまう、というか、見事にはまってしまったので、忘れないように。
※ 2020/09/16 11:12 : scivola さん指摘を受け、文言、文章を修正しました。(修正箇所が分かるように打ち消し線で修正した方が良いと思ったのですが、修正箇所がかなり多くなってしまったので、直接編集しました。)また変なところあったら、適宜修正します。
※ 2020/09/16 21:50 : 「参照渡し」を「参照の値渡し」に訂正しました。
※ 2020/09/17 21:35 : 色々悩んだ末、「参照の値渡し」という言葉は使わないように修正しました。
環境
一応、バージョン情報も。でも基本的なことなので、バージョン関係ないはずです。
- Windows 10
- Ruby 2.6
変数の値のコピーを取るとき・・・
変数の値のコピーを取るとき、ついつい代入で済ませてしまったりすること、ありませんか?
@bob = "I love you"
@carol = @bob # <= ここ!
puts "@bob = #{@bob}, @carol = #{@carol} "
# => @bob = I love you, @carol = I love you
変数の値のコピーを取るときって大抵、「元の変数の値は取っておいて、コピーした変数を使って色々操作しよう」みたいな目的のときが多いと思います。コピーした変数の方は値が変わって壊れてしまっても、元の変数は取ってあるから大丈夫ーてきな感じで。
# --- 上のつづき ---
@carol = @carol + ", too"
puts "@bob = #{@bob}, @carol = #{@carol} "
# => @bob = I love you, @carol = I love you, too
上のように、代入先の変数 @carol に文字列に追加をしても、代入元の変数 @bob の値は当然変わらないわけです。
でも、、、代入先の変数に対して、中の文字列を置換したりするなどの「破壊的操作」を行うと、代入元の値にも影響を与えてしまいます。
@bob = "I love you"
@carol = @bob
@carol.sub!("love", "hate")
puts "@bob = #{@bob}, @carol = #{@carol} "
# => @bob = I hate you, @carol = I hate you
@carol の値に変更を加えたはずなのに、@bob の値も変わってしまいました。
なんでこうなるのか、というと、Ruby では全てのものがオブジェクトで、代入演算子は、参照先をコピーする「参照渡し」 オブジェクト参照をコピーしているからです。1
@carol = @bob とすると、@carol と @bob は同じ文字列オブジェクトを参照している、ということになります。なので、@carolの方で「破壊的操作」を行うと、@bob で参照しているオブジェクトが同じなので、両方変わってしまうことになります。
ちなみに、、、同じ操作でも、sub! ではなくて subメソッドを使って変更した場合、また挙動が変わります。
# --- 上のつづき ---
@bob = @bob.sub("hate", "love")
puts "@bob = #{@bob}, @carol = #{@carol} "
# => @bob = I love you, @carol = I hate you
同じように @bob の文字列を love に変更したのに、@carol はまだ hate のままです。。。嫌われちゃったね bob。。。
でも、なんでか?
sub! の場合は、そのオブジェクトの内容を直接変更していますが、sub の方は、置換した文字列を「新しいオブジェクトとして生成」して、@bob に改めて代入しているためです。参照しているオブジェクトが @carolと一緒に参照していたオブジェクトから、新しいオブジェクトに変わったのです。
最初の例で、@carol の文字列に ", too" をつけても、@bobの値が変わらなかったのも、同じ理由です。@carol の文字列に別の文字列をつけたことで、結合した文字列の「新しいオブジェクト」が生成されて、@carol に改めて代入されたので、@bob と参照しているオブジェクトが変わったためです。
このように、単純に代入で値をコピーした、と思って安心していると、その後に行う処理によっては、代入元の値が変わってしまったり、でも処理の順序によっては大丈夫だったり、ということが起こってしまいます。
じゃあどうするの?
先にも書きましたが、変数をコピーしたい場面って、「コピー元の値を取っておきたい」かからなので、「コピー先の値を変えたら、コピー元の値も変わっちゃう」ことなんて想定しないハズです。変わっていいならコピーする必要ないわけですし。
じゃあどうしたらいいの?
ってことで出てくるのが、dupメソッドです。dup メソッドは、オブジェクトの複製、つまり、中身は同じで別のオブジェクトを生成するメソッドです。
@bob = "I love you"
@carol = @bob.dup # <== ここ、dup メソッドで@bobの値をコピーした新しいオブジェクトを作って代入する
@carol.sub!("love", "hate")
puts "@bob = #{@bob}, @carol = #{@carol} "
# => @bob = I love you, @carol = I hate you
同じように見えますが、今度は @carol だけが hate に変わりました。やっぱり嫌われる bob。。。
これは、dup メソッドで生成したオブジェクトは、@bob の持っているオブジェクトと異なっているからです。
object_id メソッドを使うと、オブジェクトが一致しているかどうかが確認できます。
@bob = "I love you"
@carol = @bob
puts @bob.object_id # => 46779560
puts @carol.object_id # => 46779560
@anna = @bob.dup
@anna.object_id # => 46931780
確かに、そのまま代入した場合は同じオブジェクトIDになっていて、dupメソッドで作ったときは別のオブジェクトIDになっています。
こうしておけば、値は同じでも完全に別のオブジェクトなので、どんな風にいじっても大元の値が勝手に変わっちゃう、ということが無くなります。
実際、「代入」だけでも、その後の操作でオブジェクトが変わることで、問題ないケースは多いと思います。でも、うっかりすると影響しちゃう、という状態はバグを誘発する恐れが高いです。ですので、
変数コピーする場合は代入はダメ、dupメソッドとか使ってちゃんと複製する
ことを無意識にできるように、がんばります(と自分に言い聞かせます・・・)
さらに加えて・・・
オブジェクトの複製には、dupメソッドの他に clone メソッドがあります。また、marshal_dump, marshal_load を使う場合もあります。
文字列オブジェクトの複製の場合は、dupだけで十分です。
cloneは、dupに加えて、特異メソッドのコピー、オブジェクトのフリーズ状態も複製します。
配列やハッシュ、クラスの複製を行う場合は、dup や clone では要素や内部変数の複製までは行わないので、不十分になります。その場合は、marshal_dump、marshal_loadを使ったり、必要に応じてメソッドのオーバーライドもしてやる必要があります。
この辺り、詳しい話は調べてみて下さい。(自分も勉強しないと・・・)
-
「参照の値渡し」という言葉は消しました。 ↩