文字列をfreezeさせるいくつかの方法


## はじめに

この記事は書籍「[プロを目指す人のためのRuby入門](http://gihyo.jp/book/2017/978-4-7741-9397-7)」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの13日目です。
本文に出てくる章番号や項番号は書籍の中で使われている番号です。

今回は文字列をfreezeさせるいくつかの方法を紹介していきます。

### 必要な前提知識

「プロを目指す人のためのRuby入門」の第7章まで読み終わっていること。

## 文字列をfreezeさせるいくつかの方法

### freezeについておさらい

Rubyの文字列はミュータブル、つまり破壊的な変更が可能なオブジェクトです（ミュータブル、イミュータブルという用語については4.7.14項を参照）。

```ruby
a = 'hello'
# 文字列を破壊的に変更する
a.upcase!
# 元の文字列が変更される
a #=> "HELLO"
```

破壊的な変更を防止したい場合は`freeze`を呼び出します（7.8.1項参照）。

```ruby
a = 'hello'

# 初期状態ではfreezeしていない
a.frozen? #=> false

# 文字列をfreezeさせる
a.freeze
a.frozen? #=> true

# freezeされると破壊的な変更ができない（エラーになる）
a.upcase! #=>RuntimeError: can't modify frozen String
```

なお、`freeze`メソッドはObjectクラスのメソッドなので、文字列以外のオブジェクトでも呼び出すことが可能です。

### マジックコメントでファイル全体の文字列をfreezeさせる

Ruby 2.3からは`frozen_string_literal`というマジックコメントが導入されました。
`# frozen_string_literal: true`というマジックコメントをファイルの先頭に書いておくと、文字列が最初からfreezeされます（以下のコードはirbではなく、適当なファイルに保存してから実行してください）。

```ruby:sample.rb
# frozen_string_literal: true

a = 'hello'

# 最初からfreezeしている
puts a.frozen? #=> true

a.upcase! #=>RuntimeError: can't modify frozen String
```

ただし、この方法でfreezeされるのは「文字列リテラル」に限定される点に注意してください（リテラルについては2.2.7項を参照）。`to_s`メソッドで数値を文字列に変換したような場合は、文字列リテラルが使われていないのでfreezeされません。

```ruby:sample.rb
# frozen_string_literal: true

# 以下はいずれも文字列リテラルなのでfreezeされる
puts 'hello'.frozen?   #=> true

puts %q!hello!.frozen? #=> true

puts <<TEXT.frozen?    #=> true
hello
TEXT

# 数値を文字列に変換したような場合はfreezeされない
puts 123.to_s.frozen? #=> false
```

### 単項演算子の`-`でfreezeさせる

Ruby 2.3からは文字列の単項演算子として`-`が追加されました。
単項演算子の`-`を使うと、freezeされた文字列が返ります。

```ruby
# 単項演算子の-でfreezeした文字列を返す
a = -'hello'
a.frozen? #=> true
```

ただし、`-`を使った場合は、freezeされた新しい文字列が返るだけで、元の文字列はfreezeされません。

```ruby
b = 'bye'

# freezeした新しい文字列を代入する
c = -b
c.frozen? #=> true

# 元の文字列はfreezeされない
b.frozen? #=> false
```

反対に、`frozen_string_literal`で文字列リテラルがfreezeされる場合は、単項演算子の`+`を使うことでfreezeしていない文字列を作成することもできます。

```ruby:sample.rb
# frozen_string_literal: true

# 通常は最初からfreezeされる
a = 'hello'
puts a.frozen? #=> true

# 単項演算子の+を使うとfreezeされない
# （厳密にはfreezeされている'bye'のfreezeされていない複製が返される）
b = +'bye'
puts b.frozen? #=> false
```

### 議論：どこまで積極的にfreezeすべきか？

将来リリースされるRuby 3ではパフォーマンスを向上させるために、文字列リテラルがデフォルトでイミュータブルになる（つまりfreezeされる）と言われています。`frozen_string_literal`というマジックコメントが導入されたのはRuby 3への移行を支援するためです。

このあたりのいきさつや議論についての詳細は以下のリンクを参照してください。

- [Feature \#11473: Immutable String literal in Ruby 3 \- Ruby trunk \- Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/11473)
- [\[Ruby\] Ruby 3\.0 の特大の非互換について \- まめめも](http://d.hatena.ne.jp/ku-ma-me/20151004/p1)

将来的にはデフォルトでイミュータブルになるとはいえ、今から積極的にその対応を進めるべきかどうかは人によって（もしくは企業や開発チームによって）判断が分かれるところだと思います。たとえば、以下の記事のように、開発チームによっては`frozen_string_literal`を積極的に付けるようにしているところもあるようです。

- 参考：[❄️frozen\_string\_literal \- Misoca開発ブログ](http://tech.misoca.jp/entry/2017/04/18/110009)

今後も長く運用・保守されていくことが明らかなRubyプログラムやRailsアプリケーションであれば、`frozen_string_literal`を積極的に付けることで、Ruby 3への対応工数を削減できる効果が期待できます。

一方、使い捨てのRubyプログラムや小さなサンプルコードであれば、`frozen_string_literal`をわざわざ付けるメリットは少ないと思います。「プロを目指す人のためのRuby入門」のサンプルコードもこちらに該当するので、書籍内では`frozen_string_literal`は付けていません。

このように、今から積極的にfreezeさせる必要があるかどうかは状況によって変わってくるため、開発チーム内で方針を議論して適切な対応を取るようにしてください。

## 次回予告

次回は正規表現関連の便利メソッドを紹介します。