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
Posted at

はじめに

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 => ["カレー"]

同じ書き方なのに、整数のbには代入されていなくて、配列のriceには代入されていました。
この違いの正体が今回のテーマです。

なぜ代入していないのに値が異なるのか

結論:「そのオブジェクトが変更可能かどうか」という性質が違うから

これだけでは理解できないので詳しく解説します。

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

まず前提として、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種類は、オブジェクトのクラス(種類)によって異なります。
Rubyではクラスのオブジェクトが変更可能かどうかは、そのクラスの設計で決まっています。

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

まずは、ミュータブルな配列(Array)から見ていきます。配列はオブジェクトの中身を変更することができます。

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)は変わりません。
だから両方のメモ(ricecurry)から"豚カツ"が追加された姿が見えます。

次に、イミュータブルな整数(Integer)を見ていきます。整数はオブジェクトの中身を変更することができません。

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により別の本体に移動)
p b.object_id  # => 1(0のオブジェクトID(識別番号)のまま。無傷)

a += 1は「0という本体を書き換える」操作ではありません。
0はイミュータブルな整数(Integer)で変更することができないので、1という本体を別の場所に置いて、aのメモを1の場所に変更する操作です。
bのメモは元の0を指したままなので、無傷です。

この両者の違いをひとことでまとめると、

  • ミュータブルは同じ本体を書き換えるので、同じ場所を指すもう一方のメモから見ても変わる。
  • イミュータブルは書き換えられず別の場所へ移るので、もう一方のメモには影響しない。

この仕組みがわかると、冒頭の謎が解けます。

冒頭の問題の解説

もう一度、最初の問題を見てみます。

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

p curry   # curry => ["カレー"]
p rice    # rice => ["カレー"]
p curry.object_id  # => 30 
p rice.object_id   # => 30

まず右端の[]で本体のオブジェクトが作成されます。そして、その本体の場所のメモをricecurryの順に書き写します。

つまり、curryriceどちらの変数も最初から同じ1つの配列を指していることになります。
あとは、配列はミュータブル(変更できる)なので、curry << "カレー"で配列に追加すると、同じ場所を指すcurryriceのメモからも変更された配列["カレー"]が確認できます。

もし別々の値を入れたい場合は

curry = []
rice = []

# 1行で書くなら
curry, rice = [], []

[]を2つ書くことで、別の場所に配列の本体を作成することができます。

a = b = 0
a += 1

p a       # a => 1
p b       # b => 0
p a.object_id  # => 3
p b.object_id  # => 1

一方、整数はイミュータブル(変更できない)です。
a += 11という別の場所へ引越ししただけです。だから冒頭のb0のままだったんですね。

まとめ

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

種類 性質 代表例
ミュータブル 中身を変更できる 配列、ハッシュ、文字列
イミュータブル 中身を変更できない 整数、シンボル、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
1

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?