Ruby の定数やfreeze の扱い方が難しい
Ruby の定数はミュータブルのため、目立たないバグを埋め込む可能性がある。 Object#freeze
を使うとオブジェクトをイミュータブル(状態変更不可)にできる。
Ruby の定数
Ruby 以外の言語では再代入させない場合があるが、Ruby の定数は Warning を出しつつも再代入できる。
irb(main):001:0> CONST = "constant string"
=> "constant string"
irb(main):002:0> CONST = "overwrite!"
(irb):2: warning: already initialized constant CONST
(irb):1: warning: previous definition of CONST was here
=> "overwrite!"
破壊的メソッドも問題なく実行されるため、定義済みの定数の内容(オブジェクト)を変更できる。
irb(main):001:0> CONST = "constant string"
=> "constant string"
irb(main):002:0> CONST << " destruct!"
=> "constant string destruct!"
これが意図した動作でない場合は Object#freeze
を使って制御しよう。
オブジェクトの凍結
オブジェクトを凍結すると “破壊的な操作” ができなくなります。
irb(main):001:0> var = "string".freeze
=> "string"
irb(main):002:0> var.frozen?
=> true
irb(main):003:0> var << " destruct!"
RuntimeError: can't modify frozen String
一度凍結されたオブジェクトは解凍できないので、どうしても変更したい場合は Object#dup
を使ってオブジェクトを複製する必要があります。
freeze メソッド
対象のオブジェクトに対する “破壊的な操作” を禁止するだけなのでオブジェクトの参照変更(代入)はできます。 いくつか不思議な動きがありますが、 freeze
はあくまでもオブジェクトを凍結するだけです。
freeze 後の代入
定数定義に合わせてオブジェクトを凍結しても、本当にやりたかった定数の状態にはならないのです。
irb(main):001:0> CONST="constant string".freeze
=> "constant string"
irb(main):002:0> CONST.frozen?
=> true
irb(main):003:0> CONST << " destruct!"
RuntimeError: can't modify frozen String
irb(main):004:0> CONST="overwrite!"
(irb):4: warning: already initialized constant CONST
(irb):1: warning: previous definition of CONST was here
=> "overwrite!"
freeze した配列やハッシュの状態変更
配列やハッシュはそのまま freeze
しても、本当にやりたかった凍結の状態にはならないのです。
irb(main):001:0> list = ['apple', 'banana', 'cherry'].freeze
=> ["apple", "banana", "cherry"]
irb(main):002:0> list << 'add'
RuntimeError: can't modify frozen Array
irb(main):003:0> list.map! {|x| x << ' rotting' }
RuntimeError: can't modify frozen Array
irb(main):004:0> list.map {|x| x << ' rotting' }
=> ["apple rotting", "banana rotting", "cherry rotting"]
irb(main):005:0> list
=> ["apple rotting", "banana rotting", "cherry rotting"]
map!
は破壊的操作なので例外が発生するが、 map
の中で配列の要素に対する破壊的操作は実行できる。 freeze
したのはあくまでも配列オブジェクトで、それを構成する文字列オブジェクトまでは凍結されない。
freeze で正しくイミュータブルにする
イミュータブルな定数を定義する
Class やModule ごと freeze することで、代入とオブジェクトの破壊を防ぐことができる。 少々、冗長な定義になるが Ruby としてはそういうことらしい。
irb(main):001:0> class MyClass
irb(main):002:1> CONST = 'constant'.freeze
irb(main):003:1> freeze
irb(main):004:1> end
=> MyClass
irb(main):005:0> MyClass.frozen?
=> true
irb(main):006:0> MyClass::CONST.frozen?
=> true
irb(main):007:0> MyClass::CONST
=> "constant"
irb(main):008:0> MyClass::CONST << ' overwrite!'
RuntimeError: can't modify frozen String
irb(main):009:0> MyClass::CONST = 'overwrite!'
RuntimeError: can't modify frozen #<Class:MyClass>
Class やModule を凍結するときの注意としては、オープンクラスとしての恩恵が得られなくなる。 Class もしくはModule の状態変更ができないというのは、定数やメソッドを定義できない状態を指す。
irb(main):001:0> class MyClass
irb(main):002:1> CONST = 'constant'.freeze
irb(main):003:1> freeze
irb(main):004:1>
irb(main):005:1* def func
irb(main):006:2> true
irb(main):007:2> end
irb(main):008:1> end
RuntimeError: can't modify frozen class
必要なオブジェクトが定義される前に freeze
するとおかしなことになる。ということで、使いどころに注意!
イミュータブルな配列やハッシュを定義する
配列やハッシュを構成する要素を凍結すれば良いだけ。
配列を要素ごと freeze する
やはり冗長な定義だが、諦めて .map(&:freeze)
すればOK.
irb(main):001:0> list = ['apple', 'banana', 'cherry'].map(&:freeze).freeze
=> ["apple", "banana", "cherry"]
irb(main):002:0> list.map! {|x| x << ' rotting' }
RuntimeError: can't modify frozen Array
irb(main):003:0> list.map {|x| x << ' rotting' }
RuntimeError: can't modify frozen String
ハッシュを要素ごと freeze する
どうしても冗長な定義だが、頑張って freeze
する。
irb(main):001:0> hash = { a: 'apple', b: 'banana', c: 'cherry' }.freeze
=> {:a=>"apple", :b=>"banana", :c=>"cherry"}
irb(main):002:0> hash.each_value(&:freeze)
=> {:a=>"apple", :b=>"banana", :c=>"cherry"}
irb(main):003:0> hash.each_value {|v| v << ' rotting' }
RuntimeError: can't modify frozen String
一行で定義できなくて、そろそろツライ。