世界を動かす技術を、日本語で。

Ruby 3.4のフローズン文字列リテラル: Rails開発者が知っておくべきこと

概要

Ruby 3.4で frozen string literals のデフォルト化に向けた 移行が開始。 現状は 警告がオプトイン 方式で、既存のRailsアプリは そのまま動作。 パフォーマンス向上や将来の互換性のため 対応が推奨段階的移行 のため、十分な 準備期間 が確保。 対応方法や注意点を 具体的に解説

Ruby 3.4で始まるfrozen string literals移行

  • Ruby 3.4 からfrozen string literals(文字列リテラルの不変化)の 段階的導入 開始
  • 既存の Railsアプリはそのまま動作、互換性維持
  • 警告はオプトイン (自分で有効化しない限り表示されない)
  • パフォーマンス向上 やメモリ削減効果あり
  • 今後数年かけて 完全移行 予定

3段階の移行計画

  • Ruby 3.4(現状) ・警告は オプトイン (Warning[:deprecated]=trueで有効化)
  • Ruby 3.7(将来) ・警告が デフォルトで表示
  • Ruby 4.0(将来)frozen string literalsがデフォルト となり、全リテラルが不変

Ruby 3.4で実際に変わること

  • デフォルトでは 何も変化なし、コードは従来通り動作
  • deprecation warnings を有効化すると、将来エラーになる箇所に 警告表示
  • 例:
    Warning[:deprecated] = true
    csv_row = "id,name,email"
    csv_row << ",created_at" # => 警告発生
    
  • 警告は自分で明示的に有効化 しない限り表示されない

なぜ対応が必要か

  • パフォーマンス向上 ・同じ文字列リテラルが 自動的に共有 され、メモリ効率化 ・GC負荷の 最大20%削減、文字列大量生成時の高速化
  • 依存gemの影響 ・自分のアプリよりも gemのコードが先に警告対象 となる場合が多い ・アップデートや対応が必要

“Chilled Strings”の仕組み

  • frozen_string_literal コメントが無い場合、Ruby 3.4では "chilled"状態 の文字列
  • 最初の変更時に警告、その後は 通常通り変更可能
  • 互換性維持しつつ、将来の移行に備える仕組み

Railsアプリでの警告検出方法

  • 開発環境で警告有効化config/environments/development.rbWarning[:deprecated]=true設定 ・または、RUBYOPT="-W:deprecated"でサーバ起動
  • テストスイートで検出spec/spec_helper.rbtest/test_helper.rbWarning[:deprecated]=truebundle exec rspecなどで警告箇所洗い出し
  • デバッグモード活用ruby --debug=frozen-string-literal your_script.rbで詳細な発生箇所特定

代表的な修正パターン

  • 文字列ビルド時の対応url = +"https://"のように +演算子で可変文字列生成+str構文は、frozenなら複製、mutableならそのまま返却
  • 破壊的変更の回避gsub!squeeze!の代わりに非破壊的メソッドを利用 ・例:filename.gsub(...).squeeze(...)
  • 文字列補間は安全"/#{controller}/#{action}"のような補間は新規生成されるため警告なし

新規コードの推奨方針

  • マジックコメントからの脱却 ・新規コードは 常に文字列を不変前提 で設計 ・上記の修正パターンを参考に実装

既存Railsアプリの移行戦略

  • マジックコメントは急いで削除不要 ・既存ファイルの互換性維持
  • 警告はCI等で徐々に解消 ・段階的に修正、gemのアップデート優先
  • CI/CDで警告検出自動化 ・例:.github/workflows/ruby.ymlRUBYOPT="-W:deprecated" bundle exec rspec

Ruby 3.4へのアップグレードは安全か

  • デフォルトで何も壊れない
  • 警告は自分で制御可能
  • 数年単位の移行期間
  • 逃げ道も用意RUBYOPT="--disable-frozen-string-literal"で一時的に警告無効化 ・特定ファイルで# frozen_string_literal: falseも可能

移行スケジュール

  • 今(Ruby 3.4) ・通常通り利用、警告は開発時に有効化推奨
  • Ruby 3.7まで ・警告対応を自分のペースで進める
  • Ruby 3.7 ・警告がデフォルト表示
  • Ruby 4.0 ・frozen string literalsが完全デフォルト化

まとめ

  • Ruby 3.4の警告は最初の一歩
  • パフォーマンス向上と将来互換性のため早期対応推奨
  • 段階的移行でリスク最小化
  • 準備期間を活かし、計画的な修正を推進

参考情報

  • Ruby 3.4.0 Release Notes
  • Feature #20205: Enable frozen_string_literal by default
  • The Future of Frozen String Literals 05 Jul 2025
  • Ruby/Rails/スケーラブルバックエンド構築に関する最新情報はブログ購読推奨

Hackerたちの意見

今日知ったんだけど、Rubyにはミュータブルな文字列があって、(発表された変更があるまでは)デフォルトでミュータブルだったんだよね(変更はリテラル文字列にだけ影響するけど、非リテラル文字列はまだミュータブルのまま)。Pythonはずっと不変の文字列しかなかったんだよね。

ユニコードのことは聞かないでね。

Pythonにはミュータブルなデフォルト引数もあるよ(https://docs.python-guide.org/writing/gotchas/#mutable-defau...)、これもデフォルトだしね。

いいね、おめでとう!俺は2005年にこれを学んだよ。

文字列は引き続きデフォルトでミュータブルのままだよ。リテラル文字列で作られた文字列だけがそうじゃないけどね。

Rubyでは、小さな不変の文字列には:symbolを使うことが多いよね。<<は文字列や配列のインプレース追加演算子で、+はコピーを作るために使う。だから、+=を使うと新しい文字列が作られて、変数が再バインドされる。

変更後も文字列はデフォルトで可変のままだよ。ただし、文字列リテラルは常に凍結されるようになる(これはしばらくファイルレベルのオプトインだった)。

成功したJITや動的型付けのスクリプト言語は、コンパイルされたり静的型付けの低レベル言語からの最適化が必要だって気づく未来があるのかな?Rubyが最初から複雑な機能を持ってたら、今ほど成功してたかな?それとも、どの言語も最初はシンプルな状態から始まって、開発者を引き込むために、言語が有名になるまで待って、そこから他の大きなプログラミング言語に似てくるのかな?

実際、逆だと思うよ。この場合、スクリプト言語では文字列が不変であることがすごく一般的で、ミュータブルな文字列は通常、低レベル言語でしか使えないことが多いんだ。Rubyがこの機能を持ってたことに驚いてるよ。

コンパイルされたり静的型付けの低レベル言語からのすべての最適化 ミュータブルな文字列は、コンパイルされたり静的型付けの低レベル言語でも全然可能だよ(特に難しくもないしね)。ただ、特にパフォーマンスが良いわけじゃなくて、時には罠になることもある。 > 最初からそんな複雑な機能 逆に、ミュータブルな文字列はより複雑な機能だと言えるよ。デフォルトでそれを取り除くことで、言語がシンプルになるか、少なくともその複雑さを見つけるために手間をかけることになるんだ。

ErlangはJITコンパイルされてて、動的で、すべての用語は不変なんだ。

ちなみに、OCamlも読み書き可能な文字列から読み取り専用の文字列に移行して、バッファをその読み書き可能な文字列にしたんだよね。これは2014年9月にリリースされた4.02で導入された。ちょっとゴタゴタした記憶があるけど、最終的にはそんなに大変じゃなかったと思う。静的型チェックが役立って、使い方を見つけるのに助けになったんじゃないかな。古いコードを動かせるようにするスイッチもあったよ(文字列とバッファを入れ替え可能にするための)。

ちょっと指摘させて:bytesが読み書き可能な文字列で、BufferはStringBuilderみたいなものだよ。

ちなみに、OCamlも読み書き可能な文字列から読み取り専用の文字列に移行したんだ。 Rubyはそうじゃなくて、特別なリテラルの扱いなしに凍結できる可変文字列から、すべての文字列リテラルが凍結された可変文字列に移行してるんだよ。

Twitterでこれをやったのから、ちょうど15年ちょっと経ったね。

このスレッドには可変文字列についての誤解が多すぎる…

何が?

これって裏でどうなってるの?Rubyはアプリケーション内のすべての文字列の巨大なマップを保持して、新しい文字列と照らし合わせて重複を排除できるか確認してるの?それとも、各ユニークな文字列の参照カウントを保持して、各文字列インスタンスの解放時にセットの検索が必要になるの?巨大なセットでの検索は結構高コストだよね!

リテラルはパース時に識別されるよ。fooLit = "foo" fooVar = "f".concat("o").concat("o") これによってfooLitはパース時に凍結される。この状況では、"foo"、"f"、"o"が凍結された文字列として存在し、fooLitとfooVarは異なる文字列になる。fooVarは実行時に作られたからね。凍結された文字列に存在する文字列を作っても、新しいものは作られないよ。

たとえ文字列の重複排除ができなくても、可変文字列リテラルは実行時にリテラルに出会うたびに新しい文字列を作らなきゃいけないってことだよね。メソッド内にリテラル文字列があったら、そのメソッドを呼ぶたびに新しい文字列が作られる。ループの中にあったら、毎回新しい文字列が作られる。そんな感じ。イミュータブルな文字列リテラルなら、文字列リテラルを再利用できるんだ。

Pythonでは、文字列リテラルは親オブジェクトの定数スロットに保存されるから、実行時にはVMがそのインデックスの値を返すだけなんだよね。Rubyにはすでに不変のインターニング文字列として機能するシンボルがあるから、凍結されたリテラルはそれに乗っかる形になるかも。凍結された文字列は内部的にはシンボルとして扱われるんじゃないかな。

これって裏でどう動いてるの?Rubyはアプリケーション内のすべての文字列の巨大なマップを保持して、新しい文字列と照合して重複を排除するの? 1. 文字列にはフラグ(FL_FREEZE)があって、文字列が凍結されるときに設定される。これは文字列が変更されるときにチェックされて、変更を防ぐんだ。2. 凍結された文字列のためのインターネット文字列テーブルがある。 > 各ユニークな文字列に対して参照カウントを保持して、各文字列インスタンスの解放時にセットルックアップが必要なの?これについてはあまり自信がないな。実装をちょっと見てみたけど、これに関しては確信が持てない。単に削除してるように見えるけど、それは正しくないと思う。何か見落としてる気がするな。Rubyの内部を掘り下げるのは年に一、二回だからね :)

まあ、これは可変文字列と不変文字列の戦争ってわけじゃないし、Rubyが遅れてきたってわけでもないよ。これは、宣言するたびに文字列を割り当てるのを避けるためで、デフォルトで凍結されるからね。主にGCのための大きな最適化なんだ。以前は、変更しないつもりなら手動で最適化しなきゃいけなかったんだよね。例えば、

以前

def my_method do_stuff_with("My String") # 毎回1回の割り当て end

以前の最適化

MY_STRING = "My String".freeze # 初期化時にGCが早めに行われる2回の割り当て def my_method do_stuff_with(MY_STRING) end

以降

def my_method do_stuff_with("My String") # 最初の呼び出しで1回の割り当て end でも、この変更は文字列の操作を複雑にする面もあって、不変の操作に傾くとたくさんの文字列を割り当てることになりがちなんだよね。 foo.upcase.reverse # VS bar = foo.dup bar.upcase! bar.reverse! だから、今は意図的にやらなきゃいけないんだ。 my_string = +"My String" # これは凍結されてない 凍結された文字列リテラルはかなり前からあって、"frozen_string_literal: true"のコメントでファイルごとに有効にできるようになってる。コミュニティでは推奨されてる方法で、ほとんどのコードベースではデファクトスタンダードになってるよ。コード品質ツールのRubocopでも一般的に強制されてるしね。可変と不変の違いはよく知られてるし、言語の一部だから、みんなその詳細を知っておくべきだよね。ちょっと驚いたのは、彼らが本当の凍結文字列リテラルに向けてこんなに長い道のりを考えたことかな。すでに"frozen_string_literal: true"のコメントで進行中だったのに。おそらく、コードに「触れずに」適切な警告を追加するためかもね。私はファイルごとの明示的なコメントが好きなんだ。依存関係については、Rubyのバージョンアップでデフォルトで凍結文字列リテラルが追加されるのはかなりのフィルターになってるよ。まあ、Rubyはまだまだ元気だし、それが大事だね。

ちょっと遅れてきた感じだね。Common Lispもリストの扱いに関して似たようなことがあるよ。具体的には、'(1 2 3)みたいな静的リストを作るのは珍しくないんだ。ただ、これをやると、データに対して他の場所でどんな操作ができるかに影響が出るんだよね。ちょっと遅れてきたと言ったのは、Lispの世界ではあまり興味を持たれていなかったパーティーがあったとは言えないからだよ。:D

ちょっと驚いたのは、こんな長い道のりを考えたことだね。最初の計画では3.0で大きな変更をする予定だったんだけど、一度にコードが壊れすぎるからその計画はキャンセルされたんだ。それで、移行を楽にするためにこの段階的なプランを提案したんだ。興味があればトラッカーのディスカッションを見てみてね: https://bugs.ruby-lang.org/issues/20205

私はよくこういうことをやるんだ。 SUB_ME = ':sub_me'.freeze def my_method(method_argument) foo = 'foo_:sub_me' foo.sub!(SUB_ME, method_argument) foo end これ、# frozen_string_literal: trueがなければ、アプリケーションがロードされるときに文字列を割り当てると思う(2回かもしれない)し、実行時にも別の文字列を割り当てて、それを変更することになるんだ。これは、

frozen_string_literal: true

FOO = 'foo_:sub_me' SUB_ME = ':sub_me' def my_method(method_argument) FOO.sub(SUB_ME, method_argument) end よりも良さそうだよね。なぜなら、アプリケーションがロードされるときにFOOに凍結された文字列が割り当てられ、実行時にそれをfooにコピーして、そのコピーを変更することになるから。つまり、メモリに残るのは2つの文字列(FOO、SUB_ME)と、GCされなきゃいけない1つの文字列(戻り値)になってしまう。これに対して、メモリに残るのは1つだけ(SUB_ME)で、GCされるのは1つ(foo/戻り値)だけになるんだ。特に、FOOがmy_methodの中でしか使われない場合はそうだよね。もしmy_other_methodでも使われていて、両方のメソッドが同じベースの文字列を使うのが論理的に意味があるなら、広いスコープの定数を使うのが得策だよね。アプリケーションでこれが妥当に思える理由は、メソッドが文字列を定義して変更して、それを送るからなんだ。これが主にうまくいくのは、小さなチームで働いているからなんだ。表向きは凍結された文字列を送るべきだけど、実際にはあまりそうしないんだ。私のルールは、定義されたコンテキストの外で文字列を変更しないことだから、それはそれで妥当だと思う。私が間違っているのか、あるいは他にもっと一般的なパターンがあって、これが望ましい理由があるのか、考えてみてもいいかな?おそらく、# frozen_string_literal: falseをファイルに追加すればいいだけだから、これは文句じゃないよ。ただ、理由が明確でないから、知りたいなと思ってるんだ。

最近、RailsプロジェクトでRubocopの統合の一環としてこれを実装したんだけど、結構微妙なバグがたくさん見つかったよ。ただ、Ruby自体がバグの多いコードになりやすい言語だってことは言っておくね。今は、こういう問題を(ほぼ)軽減してくれる洗練されたツールがあるから助かる。

「フローズン・ストリング・リテラル」って言葉、なんか変な感じがする。文字列リテラルを変数に代入するとき、変数自体を「文字列リテラル」だとは思わないんだよね。そのフレーズはコード内の引用符の間にある文字そのもののことを指していて、定義上すでに「凍結」されてる。コードの静的な部分だよ。この変更によって、文字列リテラルを使って初期化された変数は変更できなくなるんだ。(合ってるかな?)

RubyやPython、JSみたいな言語はちょっと違うんだよね。変数は文字列を「含む」んじゃなくて、ヒープ上のオブジェクトを指してるだけなんだ。だから、my_string = same_string = "Hello World" ってやると、両方の変数は実際にはヒープ上の既存のオブジェクトを指していて、そのオブジェクトは不変なんだ。

Rubyでは、「フローズン」は一部の値のプロパティで、これによって不変になるんだ。他のクラスのオブジェクトを変更可能にアンフリーズできる部分を除けばね。(少なくとも文字列はアンフリーズできない。)この変更によって、リテラルから来る文字列の値は最初から凍結されることになる。変数のバインディングとは関係ないよ。