RubyでHashとArrayをdeep freezeするための, freeezerというgemを書きました.
そもそもRubyのfreezeとは?
Rubyのfreezeはオブジェクトへの破壊的な変更を防ぐためのメソッドで, よく定数の定義とセットで使われます.
Rubyはデフォルトで定数への破壊的変更を許可しているため, 例えば以下のような現象が起こりえます.
class Test
SOME_CONST = 'some const'
end
Test::SOME_CONST.upcase!
Test::SOME_CONST # => 'SOME CONST'
これを防ぐために,定数にイミュータブルなオブジェクトを代入する場合は freeze
して破壊的変更を禁止する場合があるわけです.
class Test
SOME_CONST = 'some const'.freeze
end
Test::SOME_CONST.upcase!
Test::SOME_CONST # => raise RuntimeError: can't modify frozen String
ArrayやHashオブジェクトの場合は中身までfreezeする必要がある
Stringをfreezeしたい場合などは上記の通り単純にfreezeすればいいのですが, ArrayやHashオブジェクトをfreezeしたい場合にはもう少し注意になります.
例えば以下のようにArrayをfreezeしてもArrayの参照しているオブジェクトまではfreezeされません.
SOME_ARRAY = ['one', 'two', 'three'].freeze
# Arrayへの破壊的変更は禁止される
SOME_ARRAY.push('four') # => raise RuntimeError: can't modify frozen Array
# Arrayが参照しているオブジェクトへの破壊的変更は許可される
SOME_ARRAY[0].upcase!
SOME_ARRAY # => ['ONE', 'two', 'three']
もし本当にArrayオブジェクトの参照しているオブジェクトまでイミュータブルにしたければ,以下のように各オブジェクトもfreezeする必要があります.
SOME_ARRAY = ['one'.freeze, 'two'.freeze, 'three'.freeze].freeze
SOME_ARRAY[0].upcase! # => raise RuntimeError: can't modify frozen String
というわけでこれを一発でできるのがfreeezerというgem
壮大な前フリになってしまいましたが, 要するにArrayとHashの中身まで一発でイミュータブルにできるのがfreeezerというgemです.
using Freeezer
SOME_ARRAY = ['one', 'two', 'three'].deep_freeze
SOME_ARRAY.push('four') # => raise RuntimeError: can't modify frozen Array
SOME_ARRAY[0].upcase! # => raise RuntimeError: can't modify frozen String
上記のように using Freezer
を呼んでもらえれば deep_freeze
※1 というメソッド一発で再帰的に中のオブジェクトまでfreezeしてくれます.
所感など
自分で書いておいてなんですが, 正直クラスにある1つか2つの定数をfreezeするためにこのgemを入れて using Freeeze
って書くかと言われるとちょっと微妙かなーという気はしています.
あとたぶん定数でStringのArrayとかHashを使いたい場合がStringじゃなくてSymbolで定義すればいいのではという気もしています(Symbolはイミュータブル).
なのでもし使うとしたら Constants
みたいな定数をまとめて定義しているファイルがプロジェクトにあって,そこで何度も中身をfreezeする処理を書く場合にはいいのかもしれません.
また,今回の deep_freeze
の元ネタはプロを目指すためのRuby入門 の8章 "モジュールを理解する" で例題として紹介されていた deep_freeze
です. こいつをもう少し真面目に書いたらどうなるかということをやってみたくてgemを作ってみました.
※1 本当は deep_freeze
じゃなくて freeeze
にしたかったんですが, 流石に怒られそうだし, deep_freeze
のほうが意味が伝わりやすいので断念しました.