Hisakeyのブログ

エンジニアが色々呟くブログです。

わかっているつもりの Ruby 参照渡しを、図解で改めて理解する

はじめに

Ruby のコードを書いていると、 「変数を渡しているのに、なぜ呼び出し元が変わるの?」 「再代入すると変わらないのはなぜ?」 挙動の問題に出くわします。

自分でも理解していたつもりですがいざ人に説明しようとすると言葉に詰まってしまいます。 Ruby の参照の値渡しについて整理してみました。

背景・動機

Rubyは、参照の値渡しというモデルで動いています。

詳しくは、こちらの記事がおすすめです。(むしろこの記事を読めば、理解できますw)

magazine.rubyist.net

よくわからなくなるケースが、

  • 配列に破壊的操作をしたら呼び出し元も変わる
  • でも再代入したら呼び出し元には影響しない
  • メソッドに渡した変数が「変わるとき」と「変わらないとき」がある

このあたりの整理のために実際にコードを書きながら、図解にしてみました。

実例・やってみたこと

1. 変数とオブジェクトの関係(参照を持つ)

a = "hoge"
b = a

# 結果は同じになります
puts "a.object_id: #{a.object_id}"
puts "b.object_id: #{b.object_id}"
a ─────────────┐
               ▼
           +---------+
           | "hello" |
           +---------+
               ▲
b ─────────────┘

a と b は同じオブジェクトを指しています

2. 破壊的変更(mutaiton)は共有される

x = [1,2,3]
y = x
y << 4

puts x # [1, 2, 3, 4]
puts y # [1, 2, 3, 4]
x ───┐
     ▼
 +------------+
 | [1,2,3]    |
 +------------+
     ▲
     └── y

y << 4   # mutate!

破壊的変更は、共有されます。

3. メソッド引数は、「参照のコピー」が渡される

def modify(arr)
 arr << 999
end

list = [1,2,3]
modify(list)

puts list # [1, 2, 3, 999]
list ────────┐
              ▼
         +-----------+
         | [1,2,3]   |
         +-----------+
              ▲
arr ─────────┘   # list の参照をコピーしただけ

こちらも破壊的変更はメソッドだとしても影響します。

4. 再代入は共有されない(ここがポイント)

def reassign(value)
  value = "changed"
end

text = "original"
reassign(text)

puts text # "original"
text ──────────┐
                ▼
          +--------------+
          | "original"   |
          +--------------+

value ───────▶ "changed" (別のオブジェクト)

呼び出し元のtextは影響を受けません。(メソッド内部で呼ばれているarrは別オブジェクトとなるため)

学び・気づき

Rubyの変数は”値”でなく”参照”を持っています” 変数とオブジェクトを混同すると混乱しますが、変数はあくまでオブジェクトの矢印

メソッドにわたすときには参照のコピーが渡されます * 破壊的変更→呼び出し元に影響します。(upcase!や、今回の配列へのpusu) * 再代入→呼び出し元に影響しない

図解で書いてみると、少し理解度が増しました。

まとめ

  • 変数はオブジェクトへの「参照」を持つ
  • メソッドには参照のコピーが渡される
  • 破壊的変更は共有される
  • 再代入は共有されない

Rubyの挙動は、上記を理解していれば迷うことはなくなりそうです。

今回の記事で、誰かに説明できるようになればよいなと思います。

最後までお付き合いいただき、ありがとうございました。