Rubyにおける引数の受け渡しは、プログラミング初心者だけでなく、他の言語から移行してきた開発者にとっても混同しやすいトピックの一つです。

「Rubyは参照渡しである」と説明されることもありますが、厳密にはその表現は正確ではありません。

この仕組みを正しく理解していないと、意図しない場所でオブジェクトの内容が書き換わってしまうといったバグを引き起こす原因となります。

本記事では、Rubyにおける変数の仕組みと、メソッド呼び出し時に実際に何が起きているのかを詳しく解説します。

Rubyの引数渡しは「値渡し」である

結論から述べると、Rubyの引数渡しは「オブジェクトの参照値」をコピーして渡す「値渡し」です。

これをプログラミング用語では「共有渡し(call by sharing)」と呼ぶこともあります。

「参照渡し」という言葉が誤解を招きやすいのは、Rubyの変数がオブジェクトそのものではなく、オブジェクトへの「名札(参照)」に過ぎないからです。

メソッドに引数を渡すとき、Rubyはその名札が指し示している「オブジェクトの場所情報(参照値)」をコピーして新しい変数に代入します。

その結果、呼び出し側の変数とメソッド内の引数は、同じオブジェクトを指し示すことになります。

この挙動を理解するために、まずは変数の正体について深掘りしていきましょう。

変数とオブジェクトの関係

Rubyでは、すべてのデータがオブジェクトとして存在しています。

変数とは、そのオブジェクトに付けられた「ラベル」のようなものです。

以下のコードを見て、変数がどのようにオブジェクトを指しているかを確認してみましょう。

Ruby
# 文字列オブジェクトを作成し、変数strに代入する
str = "Hello"

# オブジェクト固有の識別子(object_id)を確認する
puts "strのID: #{str.object_id}"

# 別の変数に代入する
other_str = str
puts "other_strのID: #{other_str.object_id}"
実行結果
strのID: 60
other_str de ID: 60

実行結果からわかる通り、strother_str は全く同じ object_id を持っています。

これは、2つの変数がメモリ上の同じ場所にあるオブジェクトを見ていることを意味します。

この仕組みが、メソッドの引数渡しにおいても同様に適用されます。

なぜ「参照渡し」だと勘違いされるのか

Rubyが「参照渡し」だと思われてしまう最大の理由は、メソッド内でオブジェクトの状態を変更すると、呼び出し元にも影響が及ぶからです。

破壊的メソッドと呼ばれる操作を行った場合の挙動を確認してみましょう。

Ruby
def append_exclamation(text)
  # 破壊的に文字列を変更する
  text << "!"
  puts "メソッド内: #{text} (ID: #{text.object_id})"
end

message = "Hello"
puts "実行前: #{message} (ID: #{message.object_id})"

append_exclamation(message)

puts "実行後: #{message} (ID: #{message.object_id})"
実行結果
実行前: Hello (ID: 60)
メソッド内: Hello! (ID: 60)
実行後: Hello! (ID: 60)

この例では、メソッド append_exclamation に渡された message の内容が、メソッド終了後も変更されています。

これは text という変数が message と同じオブジェクトを指していたため、その中身を直接書き換えた結果です。

この挙動だけを見ると「参照を渡している(参照渡し)」ように見えてしまいます。

「参照渡し」ではないことを証明する挙動

もしRubyが本当の「参照渡し」であれば、メソッドの中で変数を別のオブジェクトに差し替えたとき、呼び出し元の変数も連動して切り替わるはずです。

しかし、Rubyではそうはなりません。

以下のコードで、再代入を行った場合の結果を確認してください。

Ruby
def replace_string(text)
  # 変数textに新しいオブジェクトを再代入する
  text = "Ruby"
  puts "メソッド内: #{text} (ID: #{text.object_id})"
end

message = "Hello"
puts "実行前: #{message} (ID: #{message.object_id})"

replace_string(message)

puts "実行後: #{message} (ID: #{message.object_id})"
実行結果
実行前: Hello (ID: 60)
メソッド内: Ruby (ID: 80)
実行後: Hello (ID: 60)

実行結果を見ると、メソッド内で text = "Ruby" と再代入しても、呼び出し元の message は “Hello” のままです。

メソッド内の text というラベルが新しいオブジェクト "Ruby" に貼り直されただけであり、呼び出し元の message というラベルには何の影響もありません。

「変数が指し示す先を自由に変更できるが、それは呼び出し元には伝わらない」というこの事実は、Rubyが参照そのものを渡しているのではなく、参照値をコピーして渡している(値渡し)証拠です。

ミュータブルとイミュータブルによる挙動の違い

Rubyの引数の扱いを理解する上で、オブジェクトが変更可能(ミュータブル)か、変更不能(イミュータブル)かを知ることは非常に重要です。

種類によって、メソッド内での操作が外部に与える影響が異なります。

カテゴリ代表的なクラス特徴
ミュータブルString, Array, Hash破壊的メソッド(<<map!など)で中身を変更可能。
イミュータブルInteger, Float, Symbol, TrueClass, FalseClass一度作成されると中身を変更できない。変更は常に新しいオブジェクトの生成(再代入)となる。

数値(Integer)などのイミュータブルなオブジェクトをメソッドに渡した場合、そもそも「中身を書き換える」という操作が存在しません。

そのため、どのような操作をしても呼び出し元の変数が指す値が変わることはありません。

Ruby
def increment(num)
  # 数値に1を加える(実際には新しいIntegerオブジェクトを作成して代入している)
  num += 1
end

count = 10
increment(count)
puts count
実行結果
10

このように、数値オブジェクトを渡した場合は常に安全に扱えます。

一方で、配列やハッシュ、文字列を渡す場合は、意図せず中身が書き換わるリスクを意識する必要があります。

意図しない変更を防ぐための対策

メソッド内で引数として受け取ったオブジェクトを操作する際、呼び出し元のオブジェクトを壊したくない場合があります。

そのための主な対策を2つ紹介します。

1. オブジェクトを複製する (dup / clone)

メソッドに渡す際、またはメソッドの冒頭でオブジェクトを複製することで、オリジナルのオブジェクトを保護できます。

Ruby
def safe_append(original_list)
  # 複製を作成してから操作する
  new_list = original_list.dup
  new_list << "New Item"
  return new_list
end

my_array = ["A", "B"]
result = safe_append(my_array)

p my_array # 影響を受けない
p result   # 変更された新しい配列

dup を使用することで、my_array とは別の object_id を持つ新しい配列が生成されます。

2. 破壊的メソッドを避ける

Rubyには、破壊的な操作(<<, concat, gsub!)と、非破壊的な操作(+, gsub)が用意されています。

可能な限り非破壊的メソッドを使用し、常に新しいオブジェクトを返すように設計することが、現代的なRubyプログラミングの推奨スタイルです。

Ruby
# 破壊的(危険)
text << " World"

# 非破壊的(安全)
text = text + " World"

後者の場合、新しい文字列を作成して変数 text に再代入しているため、呼び出し元への影響はありません。

まとめ

Rubyにおける引数の受け渡しは、正確には「オブジェクトの参照値の値渡し」です。

「参照を渡しているから中身が書き換えられる」のではなく、「同じオブジェクトを指すラベルをコピーして渡しているから、共有している中身を書き換えられる」というのが正しい解釈です。

この仕組みを理解するために、以下の3点を意識しておきましょう。

  • Rubyには純粋な意味での「参照渡し」は存在しない。
  • 再代入はメソッド外に影響を与えないが、破壊的変更は影響を与える。
  • ミュータブルなオブジェクト(String, Array等)を扱う際は、意図しない副作用に注意する。

変数が何を指しているのか、今操作しているのはオブジェクトそのものなのか、それともラベルの貼り替えなのかを常に意識することで、バグの少ない堅牢なコードを書くことができるようになります。