サンプルコードでわかる！Ruby 3.4の主な新機能と変更点 Part 1・文字列の凍結に関する変更点を理解する

## はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 3.4は2024年12月25日に正式リリースされました。

この記事ではRuby 3.4で導入された変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、すべての変更点を網羅しているわけではありません。僕が個人的に「Railsアプリケーションの開発時に役立ちそうだな」や「これはトリビア的な知識として知っておくと良いかも」と思った内容をピックアップしています。本記事で紹介していない変更点も多数ありますので、以下のような情報源もぜひチェックしてみてください。

https://www.ruby-lang.org/ja/news/2024/12/25/ruby-3-4-0-released/

https://github.com/ruby/ruby/blob/v3_4_0/NEWS.md

https://product.st.inc/entry/2024/12/25/154728

- [Ruby 3.4 Advent Calender](https://zenn.dev/search?q=Ruby%25203.4%2520Advent%2520Calender&order=latest)

### 動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

```
$ ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24]
```

NOTE: Ruby 3.4.0はバージョンの表記に誤りがあったため、すぐに3.4.1がリリースされました（[参考](https://www.ruby-lang.org/ja/news/2024/12/25/ruby-3-4-1-released/)）。

### フィードバックお待ちしています
本文の説明内容に間違いや不十分な点があった場合はコメント欄から指摘 or 修正をお願いします🙏

### 本記事はPart 1とPart 2の2部構成です

記事が思いのほか長くなってしまったため、Part 1とPart 2の2部構成になっています。

- Part 1 = 文字列の凍結に関する変更点を理解する（本記事）
- Part 2 = 新機能と変更点の総まとめ

この記事を読んだらPart 2もぜひチェックしてください。

https://qiita.com/jnchito/items/eadb5f96e0fdff01bd71

それでは以下が本編です！

## frozen_string_literal: true/false が指定されていないファイルで文字列に破壊的な変更を加えると警告が出るようになった

### frozen_string_literal に関するこれまでの情報をおさらいする

Ruby 2.3で `frozen_string_literal` というマジックコメントが導入されました（Ruby 3.3じゃなくて2.3ですよ！2015年の話です）。
たとえば、以下のように `true` を指定すると、文字列リテラルで作成された文字列は凍結（freeze）され、破壊的変更を加えようとするとエラーになります。

```ruby 
# frozen_string_literal: true

a = "ruby"
a.frozen? #=> true
a.upcase! #=> can't modify frozen String: "ruby" (FrozenError)
```

このとき、あらゆる全ての文字列が凍結されるわけではありません。
「文字列リテラル」を使用した場合だけである点に注意してください。

```ruby 
# frozen_string_literal: true

# 文字列リテラルを使った場合は凍結される
"ruby".frozen?  #=> true
'ruby'.frozen?  #=> true
%{ruby}.frozen? #=> true
?a.frozen?      #=> true
<<TEXT.frozen?  #=> true
ruby
TEXT

# 文字列リテラルなしで生成された文字列は凍結されない
[1, 2, 3].join.frozen? #=> false
"ruby".dup.frozen?     #=> false
("hi!" * 10).frozen?   #=> false
String.new("ruby").frozen? #=> false

# 式展開を含む文字列リテラルも凍結されない
"#{a}!".frozen? #=> false
```

Ruby 3.3まではデフォルト（`frozen_string_literal`の指定無し）は、「文字列リテラルを使った場合でも文字列は凍結されない」でした。
なので、`frozen_string_literal`に`false`を指定することはほとんど意味がありませんでした。

```ruby 
# Ruby 3.3 のデフォルトでは文字列リテラルはfreezeされない
a = "ruby"
a.frozen? #=> false
a.upcase!
a #=> "RUBY"
```

もともと `frozen_string_literal: true` というマジックコメントは、将来的にデフォルトで文字列リテラルを凍結された状態にするための移行措置として導入されたのですが、影響範囲が非常に大きいため、その仕様変更を実施するためのスケジュールは未定になっていました。

### Ruby 3.4では破壊的変更を加えると警告が出るようになった
ですが、いよいよその仕様変更に向けた動きが始まりました。
その第一弾としてRuby 3.4では、`frozen_string_literal`のマジックコメントが付いていないファイルで文字列（文字列リテラルで作成されたもの）に破壊的変更を加えようとすると警告が出ます。

```ruby 
# Ruby 3.4 のデフォルトでも文字列リテラルはfreezeされないが、破壊的変更を加えると警告が出る
a = "ruby"
a.frozen? #=> false
a.upcase! #=> warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)

# 破壊的変更自体はこれまで通り適用される
a #=> "RUBY"
```

ただし、この警告は`-w`オプションを付けて`ruby`コマンドを実行するか、`RUBYOPT=-W`の環境変数を指定しないと表示されない点に注意してください。

``` 
# オプションなしだと警告は出ない
$ ruby sample.rb

# -w オプションを付けると警告が出る
$ ruby -w sample.rb
sample.rb:3: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)

# もしくは RUBYOPT=-W の環境変数を指定する
$ RUBYOPT=-W ruby sample.rb
sample.rb:3: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
```

警告メッセージにあるように、`--debug-frozen-string-literal`のオプションを追加すると、文字列の定義場所に関する情報が出力されます。

``` 
$ ruby -w --debug-frozen-string-literal sample.rb
sample.rb:3: warning: literal string will be frozen in the future
sample.rb:1: info: the string was created here
```

ただし、`frozen_string_literal`で`false`を指定すると、従来通りの挙動（＝文字列リテラルが凍結されない）になり、警告も出ません。

```ruby 
# frozen_string_literal: false

a = "ruby"
a.frozen? #=> false
# Ruby 3.4でも警告は出ない
a.upcase!
a #=> "RUBY"
```

### 実行時オプションでアプリケーション全体の挙動を制御する

もしくは、`frozen_string_literal`が未指定でも、`ruby`コマンドに`--disable-frozen-string-literal`オプションを指定すると、従来通りの挙動（かつ警告無し）になります。

```
# --disable-frozen-string-literal を付けると frozen_string_literal が
# 未指定でも従来通りの挙動になり、警告も出ない
$ ruby -w --disable-frozen-string-literal no_magic_comment.rb
```

なお、`--disable-frozen-string-literal`オプションは`frozen_string_literal`が未指定の場合の挙動を変えるだけです。
`frozen_string_literal: true`を指定したファイルであれば、その指定が有効（＝文字列リテラルが凍結される）になります。

```
# frozen_string_literal: true が指定されていれば、文字列リテラルは凍結される
# （--disable-frozen-string-literal オプションの影響を受けない）
$ ruby -w --disable-frozen-string-literal frozen_true.rb 
frozen_true.rb:4:in 'String#upcase!': can't modify frozen String: "ruby" (FrozenError)
        from frozen_true.rb:4:in '<main>'
```

また、この反対の働きを持つ `--enable-frozen-string-literal` というオプションもあります。
このオプションを付けると `frozen_string_literal` が未指定の場合は `frozen_string_literal: true` を指定したのと同じ挙動になります。

```
# --enable-frozen-string-literal を付けると frozen_string_literal が未指定だと
# 文字列リテラルが凍結される
$ ruby -w --enable-frozen-string-literal no_magic_comment.rb
no_magic_comment.rb:4:in 'String#upcase!': can't modify frozen String: "ruby" (FrozenError)
        from no_magic_comment.rb:4:in '<main>'
```

`frozen_string_literal: false`を指定したファイルであれば、その指定が有効（＝文字列リテラルが凍結されない）になります。

```
# frozen_string_literal: false が指定されていれば、文字列リテラルは凍結されない
# （--enable-frozen-string-literal オプションの影響を受けない）
$ ruby -w --enable-frozen-string-literal frozen_false.rb 
```

参考文献: [プロと読み解くRuby 3\.4 NEWS \- STORES Product Blog](https://product.st.inc/entry/2024/12/25/154728)

### 今後、Rubyの文字列はどうなっていくのか？

具体的なスケジュールは出ていませんが、将来的に以下のような段階を経て文字列リテラルがデフォルトで凍結状態になるようです（[参考](https://bugs.ruby-lang.org/issues/20205)）。

- リリースR0: `-w`オプションを付けると警告が出る（Ruby 3.4）
- リリースR1: `-w`オプションなしでも警告が出る（時期未定）
- リリースR2: デフォルトで文字列リテラルが凍結状態になる（時期未定）

というわけでみなさん、警告を見つけたらどんどんコードを修正していきましょう・・・と言いたいところですが、本記事の執筆時点ではissueを見るとコミッタ/コントリビュータのみなさんの間では、メリット・デメリットやこの仕様変更に関するインパクトに関して様々な意見が出ているようで、今後の方向性がハッキリと定まるのにはもう少し時間がかかるような気がしています（個人の感想です）。

https://bugs.ruby-lang.org/issues/20205

参考：Google翻訳ページ

https://bugs-ruby--lang-org.translate.goog/issues/20205?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=ja&_x_tr_pto=wapp

なお、文字列リテラルをデフォルトで凍結状態にするのは、Rubyのパフォーマンス改善が第一の目的のようです。上記のissueで起票者のbyroot氏は「5%程度のパフォーマンス改善が見込める」と書いていますが、issue内には「プロジェクトによっては悪化するケースもある」という意見もあり、本当のところははたして？という感じです。

## frozen_string_literal: true/false が指定されていないファイルで、 String#+@ メソッドを使うと複製された文字列を返すようになった

Ruby3.4では、`frozen_string_literal`が指定されていないファイルで文字列リテラルを使うと、その文字列は内部的にチルド文字列（chilled string）として扱われます。

「チルド」というのは冷蔵庫とかでよく使われる、あの「チルド」です。
つまり「まだ完全な凍結（frozen）には至っていない」という意味ですね。
おそらく「今はまだ凍結されていないが、そのうち完全に凍結されますよ」というニュアンスで命名されたのではないかと推測します。

> 「チルド」とは、「傷みやすい食料品・長持ちさせたい肉などを、約0度の温度設定で冷やしたままの状態にすること」を意味している言葉です。
>
> 「チルド」は英語で“chilled”と表記しますが、「冷却した・冷却された」という意味合いを持っています。
>
> [「冷蔵」と「チルド」の違いとは？分かりやすく解釈 \| 意味解説辞典](https://meaning-dictionary.com/%e3%80%8c%e5%86%b7%e8%94%b5%e3%80%8d%e3%81%a8%e3%80%8c%e3%83%81%e3%83%ab%e3%83%89%e3%80%8d%e3%81%ae%e9%81%95%e3%81%84%e3%81%a8%e3%81%af%ef%bc%9f%e5%88%86%e3%81%8b%e3%82%8a%e3%82%84%e3%81%99%e3%81%8f/)

先ほども説明した通り、チルド文字列に対して破壊的変更を加えると警告が出ます。

```ruby 
# frozen_string_literal を指定しておらず、なおかつ文字列リテラルを使っているｗので、aはチルド文字列
a = "ruby"

# チルド文字列は凍結していない
a.frozen? #=> false

# が、チルド文字列に破壊的変更を加えると警告が出る
a.upcase! #=> warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
```

ところで、Rubyには`String#+@`というメソッドがあります。
「`"abc" + "def"`みたいに使う、あの`+`のこと？」と思う人もいるかもしれませんが、それとは違います。
`String#+@`は単項演算子としての`+`です。

```ruby 
a = "ruby"
# 単項演算子としての+を使う
b = +a
b #=> "ruby"
```

数値データならともかく、文字列に対して単項演算子の`+`を使う機会はほとんどないと思います。
僕も明示的に使った記憶がほぼありません。

Ruby 3.3の公式リファレンスでは、`+@`メソッドについて以下のような説明が書いてあります。

> self が freeze されている文字列の場合、元の文字列の複製を返します。 freeze されていない場合は self を返します。
>
> https://docs.ruby-lang.org/ja/3.3/method/String/i/=2b=40.html

ところが、Ruby 3.4ではfreezeされた文字列だけでなく、チルド文字列の場合も元の文字列の複製を返します。
以下のコードを実行した場合に、Ruby 3.3と3.4で挙動がどう違うか確認してみましょう。

```ruby 
# frozen_string_literal を指定していないので、Ruby 3.4の世界では a はチルド文字列
a = "ruby"
b = +a
# 参考：b はチルド文字列ではない（文字列リテラルから生成されていない）ので破壊的変更を加えても警告は出ない
b.upcase!

puts a
puts b
```

```
# Ruby 3.3の場合
$ ruby sample.rb
RUBY
RUBY
```

```
# Ruby 3.4の場合
$ ruby sample.rb
ruby
RUBY
```

ご覧のとおり、Ruby 3.4では`puts a`の結果が小文字の"ruby"になりました。
この結果から、以下の違いがあることがわかります。

- Ruby 3.3では`+a`はselfを返す（`a`は凍結されていないため）
- Ruby 3.4では`+a`は複製された文字列を返す（`a`はチルド文字列であるため）

一方、`-@`（単項演算子の`-`）を使った場合は挙動は変わりません。
`-@`の仕様は以下の通りです。

> self が freeze されている文字列の場合、self を返します。 freeze されていない場合は元の文字列の freeze された (できる限り既存の) 複製を返します。
>
> https://docs.ruby-lang.org/ja/3.3/method/String/i/=2d=40.html

以下のコードを実行すると、Ruby 3.3も3.4も、`a.equal?(b)`の結果は`false`になります。
つまり、「`a`は凍結されていないため、凍結された文字列の複製を返す」という結果になっています。

```ruby 
# frozen_string_literal を指定していないので、Ruby 3.4の世界では a はチルド文字列
a = "ruby"
b = -a
# b は凍結されているので破壊的変更は適用できない
# b.upcase!

# 同一オブジェクトかどうかを判定する。結果はRuby 3.3も3.4もfalse
puts a.equal?(b) #=> false

# つまり、両者のobject_idは異なる
puts a.object_id #=> 16
puts b.object_id #=> 24
```

文字列に対して単項演算子の`+`や`-`を使うことはあまり多くないと思いますが、オブジェクトの同一性が重要になる場合は、こうした挙動の違いに注意してください。

### 参考：チルド文字列かどうか判定するAPIはない
Ruby 3.4では「チルド文字列」という新しい概念が登場しましたが、以下のような理由により、プログラム上でチルド文字列かどうかを判定するAPIは用意されないようです。

https://bugs.ruby-lang.org/issues/20205#note-41 のChat GPT訳

> 「chilled strings に対する Ruby の API（例えば str.chill や String.chill(str) のようなもの）は可能でしょうか？」
>
> 技術的には非常に簡単に実装できますが、それには別途フィーチャーリクエストが必要になります。
>
> また、1つの欠点として、現在 chilled string は内部的な概念であり、将来的に削除やクリーンアップが可能なものです。しかし、これをユーザーに公開すると、永続的に維持しなければならなくなります。そのため、長期的な観点で見ると、利点がメンテナンスの負担を上回るかどうかは議論の余地があります。

## Symbol#to_s で返ってくる文字列がチルド文字列になった

Ruby 3.4では `Symbol#to_s` で返ってくる文字列がチルド文字列になりました。
そのため、シンボルから生成した文字列に破壊的変更を加えると警告が出ます（要 `-w` オプション）。

```ruby 
# シンボルを文字列に変換すると、チルド文字列が返る
name = :ruby.to_s
name #=> "ruby"
name.frozen? #=> false

# チルド文字列なので破壊的変更を加えると警告が出る
name.upcase!
#=> warning: string returned by :ruby.to_s will be frozen in the future

# 破壊的変更自体はこれまで通り適用される
name #=> "RUBY"
```

警告にも書いてあるとおり、将来的に `Symbol#to_s` で返ってくる文字列は凍結状態になる予定です。

なお、この挙動は `frozen_string_literal` の有無によって変化しない点に注意してください。
すなわち、

- `frozen_string_literal` を何も指定しない場合
- `frozen_string_literal` で `false` を指定した場合
- `frozen_string_literal` で `true` を指定した場合

のいずれの場合も、 `Symbol#to_s` の戻り値はチルド文字列になります。

https://bugs.ruby-lang.org/issues/20350

## まとめ
というわけで、この記事ではRuby 3.4で導入された文字列の凍結に関する変更点をまとめて紹介してみました。

続けてPart 2の記事をどうぞ！

https://qiita.com/jnchito/items/eadb5f96e0fdff01bd71