Ruby 3.0 から文字列リテラルがデフォルトで immutable (frozen) になるかもしれないと知り、気になったので、文字列の凍結/解凍について確認してみた。
文字列の凍結
Ruby では、Object#freeze でオブジェクトを凍結(内容の変更を禁止)できる。
String は Object を継承しているので、String#freeze で凍結できる。
s = 'hoge'
s.frozen? # => false
s.freeze
s.frozen? # => true
s << 'fuga' # => RuntimeError: can't modify frozen String
文字列の解凍(Ruby 2.2以前)
凍結されたオブジェクトは解凍(内容の変更を再許可)できない。凍結状態を含め、変更を禁ずるのが「凍結」だからである。
そのかわり、Object#dup で、凍結されたオブジェクトとほぼ同じ内容の非凍結オブジェクトが得られる。
前述の通り、String は Object を継承しているので、String#dup で非凍結文字列が得られる。
s = 'hoge'
s.freeze
s2 = s.dup # => "hoge"
s2.frozen? # => false
また、String.new(frozen_string) でも、frozen_string とほぼ同じ内容の非凍結文字列が得られる。
s = 'hoge'
s.freeze
s2 = String.new(s) # => "hoge"
s2.frozen? # => false
ただ、これらのメソッドは、「非」凍結文字列のコピーが欲しいときにも使われるため、解凍を意図していることがコード上からは分かりづらい。
文字列の解凍(Ruby 2.3以降)
Ruby 2.3 で、String#+@ が導入された。インスタンスが凍結されている場合は同内容の非凍結文字列が得られ、凍結されていない場合はインスタンス自身が得られる。
s = 'hoge'
s.freeze
s2 = +s # => "hoge"
s2.frozen? # => false
対称的な挙動の String#-@ も用意されている。
s = 'hoge'
s.frozen? # => false
s2 = -s # => "hoge"
s2.frozen? # => true
これらのメソッドは意図が明確なので、コード読解の障害にならないだろう。
一見、どちらが凍結でどちらが解凍か忘れてしまいそうだが、「オブジェクトの温度が低く(-)なれば凍結され、高く(+)なれば解凍される」と考えれば覚えやすい。