はじめに
Ruby のメソッドの引数が値渡しなのか参照渡しなのかについて疑問を持ったのですが、 Rubyist Magazin を読んで理解することができました。
ただ、ここに書かれている内容では破壊的なメソッドを使った場合の挙動に矛盾が出るのではと思い調べました。
疑問に思ったこと
Rubyist Magazin 中では、Ruby ではメソッドの引数は値渡しがされる。なので、その値に対して変更を加えても呼び出し元では変数の値は変わらない。
def increment(i)
i += 1
p "#increment 内: #{i}" #=> #increment 内: 101
end
num = 100
increment(num)
p num #=> 100
ただし、配列が渡されると、参照の値渡しになるので参照から配列の要素への変更を行うと呼び出し元の変数にまで影響が及ぶということでした。
def element_change(arr)
arr[0] = 100
end
arr = [1, 2, 3]
element_change(arr)
p arr #=> [100, 2, 3]
そこで疑問が出てきました。
引数に文字列が渡る場合、値渡しであるとすると文字列にはそのコピーが渡される。そのコピーに対して破壊的な変更をした場合、呼び出し元での変数にはその変更は適用されないということになると予想しました。
def upcase(string)
string.upcase!
end
val = "abc"
upcase(val)
p val #=> "ABC"
結果はメソッド内で破壊的な変更を行ったとき、呼び出し元でも値が変更されていました。
予想と違う結果になんでなんだろうと思いました。
結論
Ruby は値渡しであって、渡されるのはオブジェクトの参照であるというのが正しいようです。
3.8.1 オブジェクト参照
『プログラミング言語Ruby』「3.8.1 オブジェクト参照」を引用
Rubyでオブジェクトを操作する時、実際に操作しているのはオブジェクト参照である。プログラムが操作しているのはオブジェクト自体ではなく、オブジェクトの参照なのである†。変数に値を代入するときには、代入先の変数の「中に」オブジェクトをコピーするのではなく、変数にオブジェクトの参照を格納しているだけである。コードを見ると、この部分ははっきりとわかるだろう。
s = “Ruby” # String オブジェクトを作り、参照を s に格納する
t = s # 参照のコピーを作って t に格納する。s と t は同じオブジェクトを参照する
t[-1] = “” # t に格納された参照を介してオブジェクトを書き換える
print s # s を介して書き換えられたオブジェクトにアクセスする。”Rub”と表示
t = “Java” # t は別のオブジェクトを参照している
print s,t # “RubJava” と表示
Rubyでメソッドにオブジェクトを渡したとき、メソッドに実際に渡されるのは、オブジェクト参照である。オブジェクト自体ではなく、オブジェクトの参照の参照でもない。言い換えれば、メソッド引数は参照渡しではなく値渡しされるが、渡される値自体はオブジェクト参照なのである。
メソッドに渡されるのがオブジェクト参照なので、メソッドはその参照を使ってオブジェクトを書き換えられる。だから、メソッドの呼び出しから戻ってきた後も変更内容は見える状態で残っている。
† CやC++を使い慣れている読者は、参照をポインタ、すなわちオブジェクトのメモリアドレスのようなものと考えるとよいかもしれない。しかし、Rubyはポインタを使っているわけではない。Rubyの参照は、不透明で実装内部で管理されている値である。Ruby には、値のアドレスを取ったり、値を間接参照したり、ポインタ演算をしたりといった機能はない。
先程の例でいうと、i にオブジェクトの参照のコピーが渡される。そして、i += 1 とすると i という新しい変数に引数の i の参照先の値 100 と 1 が加算されて 101 が代入される。
object_id を確認すると、仮引数への代入では新しいオブジェクトが作られていることがわかる。
def increment(i)
p i.object_id #=> 201
i += 1
p i.object_id #=> 203
p "#increment 内: #{i}" #=> #increment 内: 101
end
num = 100
increment(num)
p num #=> 100
一方、破壊的変更を行うと、メソッドは参照を使ってオブジェクトを書き換える。参照はコピーして渡されるが、参照先は「abc」を指している。なので、メソッドから戻ってきても、その値は書き換えられている。
def upcase(string)
p string.object_id #=> 60
string.upcase!
p string.object_id #=> 60
end
val = "abc"
upcase(val)
p val #=> "ABC"
さいごに
Rubyiest Magazin の冒頭でこのように言い切っているので、かなり混乱してしまいましたが、Ruby はオブジェクトの参照の値渡しであると覚えておけば大体間違いはなさそうです。
Rubyist Magazin
- 「値渡し (call by value)」とは、変数の値をコピーする渡し方です。
- 「参照渡し (call by reference)」とは、変数を共有するような渡し方です。
ここまで読んでいただきありがとうございました!
Twitter もやっているのでよかったらフォローいただけるとうれしいです。