Ruby 3.4のfrozen_string_literal警告の対処法|”can’t modify frozen String”を安全に直す

Ruby 3.4で文字列に出る警告やFrozenErrorの症状から対処を引く逆引き早見表
  • このエントリーをはてなブックマークに追加

スポンサーリンク

Ruby 3.4にバージョンを上げたら、今まで動いていたコードで warning: literal string will be frozen in the future という警告や、FrozenError: can't modify frozen String というエラーに出会って戸惑っていませんか?

これは Ruby 3.4 で入った frozen_string_literal(文字列リテラルの凍結)に向けた変更が原因です。「いきなりエラーになった」と身構えてしまいますが、正体を知れば対処はシンプルです。この記事では、症状から原因と対処を引ける逆引き形式で、安全な直し方をまとめます。

Ruby 3.4で文字列に出る警告やFrozenErrorの症状から対処を引く逆引き早見表
症状から対処を引く逆引き早見表。まずは自分がどの症状かを確認しましょう

まず結論:3.4では警告は「デフォルトでは出ない」

一番の誤解を先に潰しておきます。Ruby 3.4 に上げただけでは、この警告はデフォルトでは表示されません。文字列の挙動もこれまで通りで、書き換え(mutate)は普通に成功します。いきなりアプリが壊れることはありません。

警告が見えるのは、Warning[:deprecated] = true を設定したときか、ruby -W:deprecated で実行したときだけです。逆に言えば、CI でこの設定を有効にして、将来まずくなる箇所を今のうちに炙り出すのが実務の定石になります。

# 普通に実行しても警告は出ない
$ ruby app.rb

# -W:deprecated を付けて初めて警告が見える
$ ruby -W:deprecated app.rb
app.rb:2: warning: literal string will be frozen in the future

何が変わったのか:「chilled string(凍結予備軍)」の仕組み

Ruby 3.4 では、マジックコメントが無いファイルの文字列リテラルが「chilled string(=凍結予備軍)」という中間状態になりました。まだ書き換えできる(mutable)ものの、書き換えると「将来は凍結されますよ」という警告が出る、という予告状態です。

ファイル先頭に frozen_string_literal のマジックコメントがある場合は、この chilled 扱いにはなりません。つまり文字列リテラルには、いま3つの状態があります。

ファイル先頭のコメント文字列リテラルの状態書き換えたとき
コメント無しchilled(凍結予備軍)成功するが警告(-W:deprecated時)
# frozen_string_literal: truefrozen(凍結)FrozenErrorで失敗
# frozen_string_literal: falsemutable(可変)成功・警告なし
文字列リテラルの3つの状態(chilled / frozen / mutable)をマジックコメント別に示した図
マジックコメントによって文字列リテラルの状態が変わる

「コメント無し=将来 true に倒れる予定の状態」だと捉えると分かりやすいです。FrozenError が出ている場合は、すでにそのファイルの先頭に frozen_string_literal: true が付いているはずです。

逆引き早見表:症状から対処を引く

症状原因対処
literal string will be frozen in the future(警告)コメント無しファイルで文字列を破壊的に書き換えた対処①(コメント付与)or 対処②(可変化)
can't modify frozen String(FrozenError)frozen_string_literal: true のファイルで文字列を書き換えた対処②(その文字列だけ可変に)
gem/ライブラリ側で警告が大量に出る依存側の未対応(自分では直せない)対処③(一時的に無効化)

どのケースも、犯人は文字列を破壊的に書き換えるメソッドです。<<(追記)、concat、gsub! や upcase! のような ! 付きメソッド、replace あたりが定番の踏みどころです。以下、対処を順に見ていきます。

対処①:ファイル先頭にマジックコメントを付ける(推奨)

もっとも素直な対処は、ファイルの1行目に frozen_string_literal: true を宣言することです。これでそのファイルの文字列リテラルは明確に凍結され、chilled の警告は出なくなります。gem ではほぼ標準になっている書き方です。

# frozen_string_literal: true

class Greeting
  def message
    "hello"  # このリテラルは凍結される
  end
end

凍結にはメリットもあります。同じ内容のリテラルが何度も登場しても、Rubyはメモリを1回だけ確保して使い回すため、メモリ効率が上がります。加えて、意図しない書き換えを防げるので、思わぬバグの予防にもなります。

ただし注意点があります。コメントを付けたことで、これまで書き換えていた文字列が凍結され、今度は FrozenError になる可能性があります。その場合は次の対処②で、書き換えたい文字列だけを可変にします。

対処②:その文字列だけ「可変」にする

「この文字列は後から書き換えたい」という箇所は、凍結されない可変な文字列として作ります。方法は3つあり、いずれも新しく可変なStringを用意する点は同じです。

# frozen_string_literal: true

# ① 単項プラス(+)を付ける = 凍結されていれば可変なコピーを返す
buf = +""
buf << "a" << "b"   # => "ab"(書き換えOK)

# ② String.new で作る = 常に可変
buf = String.new
buf << "a"

# ③ dup で複製する = 凍結が外れた可変コピー
name = "taro".dup
name.upcase!         # => "TARO"(書き換えOK)

実務でよく使うのは、文字列を少しずつ組み立てるバッファの初期化で +""(または String.new)を使うパターンです。ちなみに単項マイナス -"str" は逆に「凍結された文字列」を返すので、混同しないように気をつけてください。

対処③:一時的に機能ごと無効化する(応急処置)

自分のコードではなく、依存している gem 側から警告が出るケースもあります。その場合は自分では直せないので、いったん機能ごと無効化して先に進みます。実行時のコマンドライン引数で切り替えられます。

# chilled string の挙動そのものを無効化して実行する
$ ruby --disable-frozen-string-literal app.rb

あくまで応急処置です。将来のRubyでは凍結がデフォルトになるので、無効化に頼り続けるのではなく、gem のアップデートを待つ・issueを立てる、といった対応と併用するのがおすすめです。

実務での進め方:CIで炙り出して一括対応する

1ファイルずつ手で直すのは現実的ではありません。既存のRailsアプリでバージョンを上げるときは、「CIで警告を可視化 → RuboCopでコメントを一括付与 → テストで壊れた箇所を潰す」の順で進めると効率的です。

まず、テスト実行時に警告を有効化して、どこで踏んでいるかを表面化させます。テストのヘルパー(test_helper.rb や spec_helper.rb)の先頭に1行足すのが手軽です。

# test_helper.rb / spec_helper.rb の先頭などで
Warning[:deprecated] = true

次に、マジックコメントの一括付与は RuboCop の Style/FrozenStringLiteralComment に任せられます。ただしこのコップの自動修正は「文字列が凍結される=挙動が変わる」ためunsafe(安全でない)扱いです。-A(全自動修正)で適用し、その後は必ずテストを回して FrozenError を潰してください。

# このコップだけを対象に、コメントを一括付与する
$ bundle exec rubocop -A --only Style/FrozenStringLiteralComment

# その後テストを回して、凍結で壊れた箇所を対処②で直す
$ bin/rails test   # または bundle exec rspec

エラーの正体さえ分かれば、あとは「どこで書き換えているか」を追うだけです。原因の切り分け方に自信がないときは、エンジニアの仕事は「知っていること」より「調べられること」も参考にしてみてください。

将来どうなる?今のうちに直す価値

この変更は、一度に切り替えないよう複数バージョンかけた移行期間として設計されています。3.4では「警告を有効にしたときだけ出る」段階ですが、今後のバージョンで「警告が常に出る」段階を経て、最終的には文字列リテラルがデフォルトで凍結される(=書き換えたら例外)方向に進む予定です。

つまり今の警告は「猶予期間のうちに直しておいてね」というアナウンスです。デフォルト凍結になったときにいきなり大量の FrozenError で慌てないよう、バージョンを上げるこのタイミングで frozen_string_literal: true を付けておくのが、いちばん安全で効率的な進め方です。メモリ効率も上がるので、直して損はありません。

まとめ

  • Ruby 3.4 の警告はデフォルトでは出ない。Warning[:deprecated] = true / -W:deprecated で可視化する
  • コメント無しファイルの文字列は「chilled(凍結予備軍)」。書き換えると警告が出る
  • 基本の対処は # frozen_string_literal: true を付けること(メモリ効率も上がる)
  • 書き換えたい文字列だけ +"" / String.new / .dup で可変にする
  • 既存アプリはCIで警告を有効化し、RuboCop -A で一括付与してからテストで潰す

Rubyやフレームワークのバージョンアップは「上げたら壊れた」で身構えがちですが、変更の意図を理解すれば怖くありません。これから開発環境を整える方は macOSにRuby on Railsをインストールする方法 を、Railsでアプリを作り始めたい方は Railsでブログアプリを作る方法 もあわせてどうぞ。

スポンサーリンク

  • このエントリーをはてなブックマークに追加