Rubyで意図せず変数の値が書き換わってしまうことについて調べてみる
引数に文字列や配列を渡す場合、メソッドの中での処理のされ方によって元の変数の値が変わってしまうことがあります。
元の変数の値が変わってしまうことについて手を動かしながら確認してみました。
例
- 数値を渡すパターン
def foo(a) a += 1 end x = 10 puts foo(x) #=> 11 puts x #=> 10 xは変更されていない
- 文字列を渡すパターン
def foo(a) a = "foo" end def bar(a) a.upcase! end x = "abc" puts foo(x) #=> foo puts x #=> abc xは変更されていない puts bar(x) #=> ABC puts x #=> ABC xは変更されている
- 配列を渡すパターン
def foo(a) a = [4, 5, 6] end def bar(a) a << 4 end x = [1, 2, 3] p foo(x) #=> [4, 5, 6] p x #=> [1, 2, 3] xは変更されていない p bar(x) #=> [1, 2, 3, 4] p x #=> [1, 2, 3, 4] xは変更されている
この挙動について文字列を使って確認します。
Fiddle::Pointerクラスというものを使うと、どのメモリ領域が使われているかを知ることができます。
Fiddle::Pointer
でどのアドレスに何が保存されているかを確認する
require "fiddle" s = "adc" cptr = Fiddle::Pointer[s] p cptr.to_i #=> 140737198084016 # "abc"が格納されている先頭のアドレス = sが指しているアドレス p cptr[0] #=> 97 # cptrの先頭アドレスに入っている値 p cptr[0, 1] #=> "a" # cptrの先頭アドレスから、1バイト分に入っている値を文字列に変換したもの # aは文字コードで97。 p cptr[1] #=> 98 # cptrの2バイト目に入っている値 p cptr[1, 1] #=> "b" # cptrの2バイト目に入っている値を文字列に変換 p cptr[2] #=> 99 # cptrの3バイト目に入っている値 p cptr[2, 1] #=> "c" # cptrの3バイト目に入っている値を文字列に変換
Fiddle::Pointer.[]
をto_i
して出力される数値は、与えられた値が文字列の場合は、値が入っている先頭のアドレス(メモリの中での位置)です。
cptr[0]
では、先頭のアドレスの中に入っている値を取得します。
cptr[0, 1]
では、その値を文字列に変換した値を返します。
参考: ASCIIコード表
cptr[1]
, cptr[1, 1]
はその次のアドレスに入っている値と、その値を文字列に変換した値を返します。
これを図で表すと次のようになります。
変数sの中には、文字列"abc"自体が入っているのではなくて、"abc"の先頭のアドレスが入っています。(Rubyソースコード完全解説 を読んでいるとこの説明は正確ではなさそうですが、だいたいのイメージはあっているのではと思います)
Fiddle::Pointer
クラスを使って、元の変数の値が変わらないパターンと変わるパターンを詳しく追っていきます。
元の変数の値が変わらないパターン
s = "abc" def foo(a) a = "foo" end foo(s) puts s #=> abc
これをFiddle::Pointer
を使って詳しく見てみます。
require 'fiddle' s = "abc" cptr = Fiddle::Pointer[s] p cptr.to_i #=> 140737198084016 p cptr[0] #=> 97 p cptr[1] #=> 98 p cptr[2] #=> 99 def foo(a) cptr = Fiddle::Pointer[a] # ・・・1 p cptr.to_i #=> 140737198084016 p cptr[0] #=> 97 p cptr[1] #=> 98 p cptr[2] #=> 99 a = "foo" cptr = Fiddle::Pointer[a] # ・・・2 p cptr.to_i #=> 140737198082816 p cptr[0] #=> 102 p cptr[1] #=> 110 p cptr[2] #=> 110 end foo(s) puts s #=> abc
コード中の1を図にすると次のようになります。
Rubyは値渡しなので、変数s
に入っていた値をそのまま仮引数のa
に渡します。
この時点では、a
も文字列"abc"
の先頭を指しています。
次にコード中の2を見ます。
a
に"foo"
を代入したことで、a
の指す先が変わりました。
a
の指す先が変わっただけなので、s
には何の影響もありませんでした。
元の変数の値が変わるパターン
s = "abc" def bar(a) a.upcase! end bar(s) puts s #=> ABC
これもFiddle::Pointer
を使って詳しく見てみます。
require 'fiddle' s = "abc" def bar(a) cptr = Fiddle::Pointer[a] # ・・・1 p cptr.to_i #=> 140737198084016 p cptr[0] #=> 97 p cptr[1] #=> 98 p cptr[2] #=> 99 a.upcase! cptr = Fiddle::Pointer[a] # ・・・2 p cptr.to_i #=> 140737198084016 p cptr[0] #=> 65 p cptr[1] #=> 66 p cptr[2] #=> 67 end bar(s) puts s #=> ABC
1の時点では先程と全く同じです。
2の時点を確認してみます。
a
の指す先はs
と変わらない状態で、指す先の文字列を変更していることが分かりました。これによって、s
が指している文字列も変わってしまいました。
String#upcase!
のように破壊的な変更をするメソッドはこのようにアドレスに保存されている文字自体を変更する挙動をするため、一度他の変数に代入していたとしても、同じアドレスを指す全ての変数が影響を受けてしまいます。
まとめ
その変数の中に本当に保持されているのが何か、を考えると個人的には理解しやすいように思いました。
数値であれば、変数に数値自身が保持されるので、a = 1; b = a
のようなことをすれば、a
とb
は全く関係がなくなります。
しかし文字列や配列、また恐らくその他のクラスの多くも、変数の中には文字列全体などではなく、先頭のアドレス(やその他の付加情報)だけ保持されています。なので、a = "abc"; b = a
としても、a
とb
の間には関係が残っているので、a
への変更がb
に影響を及ぼしうる、ということで注意して使わなければいけないのだと思いました。
補足
こういった内容の他の記事を調べていると、object_id
を使って確認しているものをよく見ます。
今までobject_id
がアドレスを指していると思っていたのですが、今回確認したところ違ったようです。ただ大体 "object_id * 2 ≒ 先頭のアドレス" という関係があったので何かしら関係はありそうです。