フィヨルドブートキャンプでチェリー本の第1版の輪読会をやっているんですが、その時出てきた「ミュータブル」と「イミュータブル」について誤解していたのでまとめたいと思います。
- ミュータブル・イミュータブルとは
- ミュータブルなオブジェクトの例
- ミュータブルなオブジェクトは生成するたびに異なるオブジェクトになる
- ミュータブルなオブジェクトの注意点
- イミュータブルなオブジェクトの例
- イミュータブルなオブジェクトはデフォルトでfreezeされている
- ちなみに
- 参考
ミュータブル・イミュータブルとは
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'
参考
るりま: