3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rubyのミュータブルとイミュータブルが分からなかったのでわかりやすく

3
Last updated at Posted at 2026-08-26

はじめに

paizaのプログラミングスキルチェックを解いていて、変数の代入について詰まった箇所を深掘りしたところ、ミュータブルとイミュータブルを理解する必要があったので記事にしました。
言語によって異なるそうですが、今回はRubyの場合を想定した深掘りになります。

以下は私が詰まったところの内容です。
a, b, curry, riceには何がはいるでしょうか?

a = b = 0
a += 1

p a       # a => ?????
p b       # b => ?????

curry = rice = []
curry << "カレー"

p curry   # curry => ?????
p rice    # rice => ?????
答え
a => 1
b => 0
curry => ["カレー"]
rice => ["カレー"]

どちらも2つの変数に同じ値を持たせたはずなのに、bは値が変わらず、riceは値が変わったように見えました。 この違いの正体が今回のテーマです。

なぜ一方だけ値が変わったように見えるのか

結論:a += 1curry << "カレー"は、見た目は似ていても「やっていること」が違うから

具体的には、a += 1は変数に別のオブジェクトを代入し直す操作、curry << "カレー"は同じオブジェクトの中身を書き換える操作です。
そして、この「中身を書き換える」操作ができるオブジェクトのことを、ミュータブルなオブジェクトと呼びます。ここが今回のタイトルにつながる部分です。
この違いを理解するために、まず「変数とは何か」から順に見ていきます。

その前に:オブジェクトとは何か

まず前提として、Rubyでは扱うデータがすべてオブジェクトです。
整数の0,1、文字列の"カレー"、配列の[1, 2]など
これらは全てオブジェクトといいます。

これらオブジェクトは変数に代入できます。
ただし、「代入」で実際に何が起きているのかが、今回の問題のポイントになります。

変数は 「値の箱」 ではなく 「オブジェクトの場所を示すメモ」

変数は「値を入れる箱」とイメージすると思います。
最初のうちは分かりやすいですが、もっと実態に近いのは次のようなイメージです。

オブジェクトは、どこかの場所に置かれている本体。変数は、その本体がどこの場所にあるか指し示すメモ

例えば、次のコードは

curry = ["カレー", "ライス"]

curryという箱に["カレー", "ライス"]が入っている」のではなく、
「どこかの場所に["カレー", "ライス"]が置かれていて、curryはそのどこかという場所を示すメモを持っている」というようなイメージです。

このイメージが効いてくるのが、変数から変数への代入です。

curry = ["カレー", "ライス"]
rice = curry

p curry  # => ["カレー", "ライス"]
p rice   # => ["カレー", "ライス"]

rice = curryは「curry["カレー", "ライス"]をコピーしてriceという箱に入れている」のではなく、
curryが持っている場所を示すメモを、riceにも書き写している」だけです。
結果として、ricecurryは同じ1つの場所を指す2枚のメモになります。

オブジェクトの本体には、1つ1つに固有の識別番号が割り振られています。2枚のメモが同じ本体を指しているかは、この番号で確かめられます。
その方法として、object_idメソッドがあります。

curry = ["カレー", "ライス"]
rice = curry

p curry.object_id  # => 1120 
p rice.object_id   # => 1120(curryと同じオブジェクトID(識別番号)を持っている)

識別番号が同じということは、2枚のメモが同じ場所を指している証拠です。(番号の具体的な数値は実行環境によって異なります)

同じ本体の場所を2枚のメモで指しているとき、片方から本体に手を加えた場合どうなるでしょうか。
ここで、冒頭の2つの操作の違いが効いてきます。

「代入し直す」か「本体を書き換える」か

冒頭のa += 1curry << "カレー"は、変数に対してやっていることが根本的に違います。ここが今回の謎の核心です。

<< は「本体を書き換える」——もう一方のメモにも伝わる

まずcurry << "カレー"から見ます。<<変数に代入しませんcurryが指すオブジェクトはそのままで、そのオブジェクト自身の中身を書き換えます。

curry = ["カレー", "ライス"]

p curry.object_id  # => 30 (["カレー", "ライス"]のオブジェクトID(識別番号))

rice = curry
curry << "豚カツ"

p curry  # curry => ["カレー", "ライス", "豚カツ"]
p rice   # rice  => ["カレー", "ライス", "豚カツ"](curryと同じ!)

p curry.object_id  # => 30(中身を変えてもIDは同じ = 同じ本体を書き換えた証拠)
p rice.object_id   # => 30(curryと同じオブジェクトID(識別番号)を持っている)

注目してほしいのは、2枚のメモは一切書き換えていないことです。ricecurryも同じ場所を指したまま、「本体」だけが変わりました。
object_idが前後で変わっていないのが、その証拠です。同じ本体を書き換えただけなので、指し示す場所(ID)は変わりません。
だから同じ本体を指すもう一方のメモ(rice)からも、"豚カツ"が追加された姿が見えます。

+=は「別の本体を代入し直す」——もう一方のメモには伝わらない

次にa += 1。これはa = a + 1の略で、変数aに新しいオブジェクトを代入し直しています。これを自己代入演算子と呼びます。
だからaが指す先が、別の本体に変わります。

a = 0
p a.object_id  # => 1(0のオブジェクトID(識別番号))

b = a
a += 1

p a  # a => 1
p b  # b => 0

p a.object_id  # => 3(a += 1により、aが別の本体(1)を指すように変わった)
p b.object_id  # => 1(bは元の0を差したまま。無傷)

a += 1は「0という本体を書き換える」操作ではありません。
01を足した結果である1という別の本体を、aに代入し直す操作です。aのメモが1の場所に書き換わる一方で、bのメモは元の0を指したままなので、無傷です。

大事な注意:これは「配列だから/整数だから」ではない

私自身勘違いしていました。
ここで勘違いしやすいのですが、ricebで結果が別れたのは「配列だから」「整数だから」ではありません。<<(本体を書き換える)を使ったか、+=(代入し直す)を使ったかという、操作の違いが原因です。

証拠に、同じ配列でも+=を使うと、もう一方のメモは変わりません。

curry = rice = ["カレー", "ライス"]
curry += ["豚カツ"]

p curry  # => ["カレー", "ライス", "豚カツ"]
p rice   # => ["カレー", "ライス"]

curry += ["豚カツ"]curry = curry + ["豚カツ"]の略で、新しい配列を作ってcurryに代入し直しています。
riceは元の配列を指したまま無傷です。冒頭のa += 1とまったく同じことが、配列でも起きています。

では、ミュータブルとイミュータブルは何のためにあるのか

「差の原因が操作の違いなら、ミュータブルとイミュータブルは関係ないの?」と思うかもしれません。ですが、しっかり関係しています。

両者の言葉を言い換えると以下になります。

  • ミュータブル = 一度できたオブジェクトの中身を、あとから変更できる
  • イミュータブル = 一度できたオブジェクトの中身を、あとから変更できない

この2種類は、オブジェクトのクラス(種類)によって決まっています。

  • ミュータブル = 配列(Array)、ハッシュ(Hash)、文字列(String)など
  • イミュータブル = 整数(Integer)、シンボル(Symbol)など

ここで効いてくるのが、さきほどの<<のような「本体を書き換える操作」です。本体を書き換える操作ができるのは、ミュータブルなオブジェクトだけです。配列は<<で中身を書き換えられますが、整数には中身を書き換える手段がありません。

a = 0
# a << 1 のような「0という本体を書き換える」操作は存在しない
a += 1  # できるのは「別の本体を代入し直す」ことだけ

だから、「curryをいじったらriceまで変わった」という一見びっくりする現象は、ミュータブルなオブジェクトを共有しているときにだけ起こりえます。イミュータブルな整数は、そもそも本体を書き換える操作がないので、こういうことは起こしようがありません。

まとめ

ミュータブルとイミュータブルを整理します。

種類 性質 代表例
ミュータブル 中身を変更できる 配列、ハッシュ、文字列
イミュータブル 中身を変更できない 整数、シンボル、true/false、nil
  • 変数は値を入れる箱ではなく、オブジェクトがどこにあるか指し示すメモ
  • <<のように本体を書き換える操作をすると、同じ本体を指すもう一方のメモにも伝わる
  • +=のように別の本体を代入し直す操作は、指す先が変わるだけなので、もう一方のメモには伝わらない
  • 本体を書き換える操作ができるのはミュータブルだけ。

(補足)

  • mutable(ミュータブル)は「変わりやすい、変更可能な」という意味
  • immutable(イミュータブル)は「変わらない」という意味でim-の打ち消しがついている。
  • オブジェクトが変更禁止かどうかを返すfrozen?メソッドがある
"カレー".frozen? # => false(変更可能)
1.frozen?       # => true(整数は常にfrozen(凍結済み))
  • 2つが同一のオブジェクトかを判定するequal?メソッドがある。==との違いとして==は「中身が同じか」、equal?は「同一オブジェクトか」
curry = ["カレー"]
rice = ["カレー"]   # 中身は同じだが、別々に作っている

# == は「中身が同じか」を見る
p curry == rice   # => true(どちらも["カレー"]で中身は同じ)

# equal? は 「同一の本体か」を見る
p curry.equal?(rice)     # => false(別の場所に作った別の本体)
p curry.object_id        # => 30
p rice.object_id         # => 40(object_idが違う = 別の本体)
3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?