プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

【Ruby】ミュータブルとイミュータブル

フィヨルドブートキャンプでチェリー本の第1版の輪読会をやっているんですが、その時出てきた「ミュータブル」と「イミュータブル」について誤解していたのでまとめたいと思います。

ミュータブル・イミュータブルとは

Rubyのオブジェクトは「ミュータブルなオブジェクト」か「イミュータブルなオブジェクト」の2種類に分けられる。

じゃあミュータブル・イミュータブルってなんやねん、という話。

  • ミュータブル:破壊的な変更が可能なオブジェクト

  • イミュータブル:破壊的な変更ができない(そもそも破壊的メソッドが定義されていない)オブジェクト

ミュータブルなオブジェクトの例

# 文字列(String)
str = 'hello'
str.upcase! #=> 'HELLO'
str #=> 'HELLO'

# 配列(Array)
ary = []
ary.push(1, 2, 3) # => [1, 2, 3]
ary # => [1, 2, 3]

# 配列(Hash)
hash = {a: 1, b: 2}
hash.merge!({c: 3}) #=> {a: 1, b: 2, c: 3}
hash#=> {a: 1, b: 2, c: 3}

ミュータブルなオブジェクトは生成するたびに異なるオブジェクトになる

ミュータブルなオブジェクトは、例え同じ文字列でも生成するたびに異なるオブジェクトとなります。 同一オブジェクトかどうかはオブジェクトIDを確かめるとわかります。

なおオブジェクトIDは実行環境で変わります。

# 同じ文字列でも生成するたびに異なるオブジェクト
'aaa'.object_id
=> 63180

'aaa'.object_id
=> 65580

'aaa'.object_id
=> 67980

異なるオブジェクトなのでそれぞれを別々の変数に代入し破壊的な変更を加えても、お互いに影響がありません。

str1 = 'aaa'
str2 = 'aaa'

str1.upcase! #=> 'AAA'
str2 #=> 'aaa'

str1.object_id == str2.object_id
#=> false

ミュータブルなオブジェクトの注意点

次のようにするとどちらの変数も同じオブジェクトを参照しているため破壊的変更を加えるとどちらも変更されてしまいます。

str1 = 'aaa'

str2 = str1
str2 # => 'aaa'

str2.upcase! #=> 'AAA'
str1 => 'AAA'

# str1もstr2も同じ'aaa'というオブジェクトを参照している
str1.oject_id == str2.object_id
#=> true

イミュータブルなオブジェクトの例

Integer, Floatなどの数値やSymbolなどがイミュータブルなオブジェクトに当たります。 一意であることを担保されているため、同じ値であればオブジェクトIDは常に同じです。

# 数値(Integer, Float)
1.object_id
=> 3
1.object_id
=> 3
1.object_id
=> 3

(1.4).object_id
=> -21617278211378382
(1.4).object_id
=> -21617278211378382
(1.4).object_id
=> -21617278211378382


# シンボル(Symbol)
:a.object_id
=> 808668
:a.object_id
=> 808668
:a.object_id
=> 808668

イミュータブルなオブジェクトはデフォルトでfreezeされている

ミュータブルなオブジェクトに対してfreezeメソッドを使うと破壊的変更ができなくなります。

一方、イミュータブルなオブジェクトはデフォルトでfreezeされています。(当たり前か、、、)

# ミュータブル
str = 'aaa'
str.frozen
#=> false

str.freeze
str.frozen?
#=> true

str.upcase!
#=> `upcase!': can't modify frozen String: "aaa" (FrozenError)

# イミュータブル
num = 1
num.frozen?
#=> true

ちなみに

「破壊的変更ができないこと」と、「変数に再代入できないこと」は別物です。ここを混同して勘違いしていました。 freezeしても「破壊的変更ができない」だけで再代入して参照するオブジェクトが変わってしまったら破壊的変更が加えられます。

僕は「再代入」を破壊的変更だと思い込んでいたので、そもそも再代入できないのでは?と誤解していました。

str = 'aaa'
str.freeze

# freezeしたので破壊的変更は加えられない
str.upcase! 
#=> `upcase!': can't modify frozen String: "aaa" (FrozenError)

str = 'aaa' # 異なるオブジェクトを再代入する
str.upcase!
#=> 'AAA'

参考

るりま: