①〜④でreplaceメソッド実行後の変数aの値はそれぞれ何になるでしょうか。この記事ではメソッドの引数と、メソッド呼び出し元の変数について動きを追って見ていきます。
①破壊的メソッドによる変更
a = "hoge"
replace(a)
def replace(str)
str.upcase!
end
②破壊的メソッド実行後、値を変更
a = "hoge"
replace(a)
def replace(str)
str.upcase!
str = "fuga"
end
③配列自体を変更
a = ["hoge", "fuga"]
replace(a)
def replace(list)
list = ["piyo", "fuga"]
end
④配列の要素を変更
a = ["hoge", "fuga"]
replace(a)
def replace(list)
list[0] = "piyo"
end
メソッドの引数は値渡し?参照渡し?
メソッド内で引数の値を変更すると元の変数の値も変わるのが参照渡し、変わらないのが値渡しです。Rubyのメソッドは値渡しなのか参照渡しなのかを検証するために引数の値をみていきます。
値渡し?
もしメソッドの引数が値渡しであれば、add(a)
実行後のaは2ではなく1のままになるはずです。
a = 1
add(a)
puts a #=> 1 #変更されていない
def add(x)
x += 1
end
aは2ではなく1になりました。addメソッド実行前後を比較して値が変わらない=メソッドの引数は値渡しであるということがわかります。
参照渡し?
次に、④のメソッド内で配列自体を変更しているケースを考えてみます。メソッドの引数が値渡しであれば、a[0]は"hoge"のまま変わらないはず。
a = ["hoge", "fuga"]
replace(a)
puts a #=>["piyo", "fuga"] #"piyo"に変わっている
def replace(list)
list[0] = "piyo"
end
結果、a[0]は"piyo"になりました。ということはメソッドの引数は参照渡しなのでしょうか?
参照の値渡し
④のケースは参照渡しのように見えますが、実は値渡しです。ただし、値渡しで渡されているのはオブジェクトの参照です。object_idを確認すると、replace実行前のaとlist[0] = "piyo"
直前の引数xは同じobject_idになっています。
配列の要素の変更は破壊的メソッドのため、オブジェクトの値そのものが変わり、replace後のaに反映されます。そのため、あたかも参照渡しをしているように見えるのです。
a = ["hoge", "fuga"]
puts a.object_id #=>70252646834320
replace(a)
puts a.object_id #=>70252646834320 # replaceメソッド実行前のaと同じID
def replace(list)
puts list.object_id #=>70252646834320 # aと同じID
list[0] = "piyo"
puts list.object_id #=>70252646834320 # aと同じID
end
では、なぜ「値渡し?」で確認したケースでは値が変わらなかったのでしょうか。
object_idの動きを確認してみます。
add(a)
実行前のaと、x += 1
直前のxは同じobject_idになっているので同じオブジェクトを参照しています。
x += 1
を実行すると、xがもとの変数aとは別のオブジェクトに変わっています。
x += 1
は非破壊的変更のため新しいオブジェクトが生成され、aには値が反映されなかったことがわかります。
a = 1
puts a.object_id #=>3
add(a)
puts a.object_id #=>3
def add(x)
puts x.object_id #=>3
x += 1
puts x.object_id #=>5 #>オブジェクトIDが変わっている
end
答え合わせ
では、①〜④のメソッド実行後のaの値を見ていきましょう。
①破壊的メソッドによる変更
これは④と同じで、破壊的メソッドによる変更を行なっているため、aの値が大文字の"HOGE"に変わります。
a = "hoge"
puts a.object_id #=>70252646922080
replace(a)
puts a.object_id #=>70252646922080
puts a #=>"HOGE" #aが大文字の"HOGE"に変更された
def replace(str)
puts str.object_id #=>70252646922080
str.upcase! # strを大文字に変更
puts str.object_id #=>70252646922080 #aと同じID
end
②破壊的メソッド実行後、値を変更
str.upcase!
までの流れは①と同じです。が、②の場合はstr.upcase!
の後にstr = "fuga"
を実行しています。
str = "fuga"
を実行するとオブジェクトIDが変わるため、aにはstr.upcase!
までの値が反映され、"HOGE"になります。
a = "hoge"
puts a.object_id #=>70252647097220
replace(a)
puts a.object_id #=>70252647097220
puts a #=>"HOGE"
def replace(str)
puts str.object_id #=>70252647097220
str.upcase!
puts str.object_id #=>70252647097220
str = "fuga"
puts str.object_id #=>70252647132520 #オブジェクトIDが変わっている
end
③配列自体を変更
④はメソッド内で配列の要素を変更する例でしたが、こちらは配列そのものを変更するケースです。list = ["piyo", "fuga"]
は破壊的な変更ではないため、新しくオブジェクトが生成され、オブジェクトIDが変わっています。つまり、もとの変数aにはlist = ["piyo", "fuga"]
が反映されず、aの値は["hoge", "fuga"]のまま変わりません。
a = ["hoge", "fuga"]
puts a.object_id #=>70252646933980
replace(a)
puts a.object_id #=>70252646933980
puts a #=>["hoge", "fuga"]
def replace(list)
puts list.object_id #=>70252646933980
list = ["piyo", "fuga"]
puts list.object_id #=>70252646967500 #オブジェクトIDが変わっている
end
④配列の要素を変更
④のオブジェクトIDの動きは上で説明したとおりです。replace(a)
後のaの値は["piyo", "fuga"]に変わります。
a = ["hoge", "fuga"]
puts a.object_id #=>70252646834320
replace(a)
puts a.object_id #=>70252646834320 # replaceメソッド実行前のaと同じID
puts a #=>["piyo", "fuga"]
def replace(list)
puts list.object_id #=>70252646834320 # aと同じID
list[0] = "piyo"
puts list.object_id #=>70252646834320 # aと同じID
end
まとめ
- Rubyのメソッドの引数は値渡しで参照を渡している(参照の値渡し)
- メソッドの引数に対して非破壊的な変更を行った場合、新しいオブジェクトが生成されるため、メソッドの呼び出し元の変数には値は反映されない
- メソッドの引数に対して破壊的な変更を行った場合、オブジェクトが持つ値自体が変更されるため、メソッドの呼び出し元の変数に値が反映される