はじめに
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にも書き写している」だけです。
結果として、riceとcurryは同じ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枚のメモは一切書き換えていないことです。riceもcurryも同じ場所を指したまま、「本体」だけが変わりました。
object_idが前後で変わっていないのが、その証拠です。同じ本体を書き換えただけなので、指し示す場所(ID)は変わりません。
だから両方のメモ(riceとcurry)から"豚カツ"が追加された姿が見えます。
次に、イミュータブルな整数(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
まず右端の[]で本体のオブジェクトが作成されます。そして、その本体の場所のメモをrice → curryの順に書き写します。
つまり、curryとriceどちらの変数も最初から同じ1つの配列を指していることになります。
あとは、配列はミュータブル(変更できる)なので、curry << "カレー"で配列に追加すると、同じ場所を指すcurryとriceのメモからも変更された配列["カレー"]が確認できます。
もし別々の値を入れたい場合は
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 += 1は1という別の場所へ引越ししただけです。だから冒頭のbは0のままだったんですね。
まとめ
ミュータブルとイミュータブルを整理します。
| 種類 | 性質 | 代表例 |
|---|---|---|
| ミュータブル | 中身を変更できる | 配列、ハッシュ、文字列 |
| イミュータブル | 中身を変更できない | 整数、シンボル、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が違う = 別の本体)