【備忘録】Rubyの定数に gsub! しても警告が出ないのはなぜか
IRBでこんなコードを試していて、不思議に思ったことを記録しておきます。
irb(main):006> HOGE = "hoge"
=> "hoge"
irb(main):007> HOGE.gsub!("hoge", "piyo")
=> "piyo"
irb(main):008> print HOGE
piyo=> nil
定数を変更したのに警告が出ななかったので、その理由を調べてまとめました。
結論
Rubyの定数は「再代入」には警告を出しますが、「破壊的変更」には警告を出さない
詳しく見てみる
再代入の場合(警告が出る)
HOGE = "hoge"
HOGE = "piyo"
# => warning: already initialized constant HOGE
再代入(==で新しいオブジェクトを代入)すると、Rubyは「定数を再定義している」と判断して警告する
破壊的変更の場合(警告が出ない)
HOGE = "hoge"
HOGE.gsub!("hoge", "piyo")
# => "piyo"
これは「HOGE が指しているオブジェクト自体を変更している」だけ。再代入ではないため、Rubyは警告を出さないとのこと
「なぜ変更できるのか」 〜freezeとfrozen_string_literal〜
実は、Rubyの文字列リテラルはデフォルトでは凍結されていないらしい
HOGE = "hoge"
HOGE.frozen? # => false
したがって、破壊的メソッド(gsub!, chop!, upcase! など)を自由に使えてしまう
凍結すればエラーになる
HOGE = "hoge".freeze
HOGE.gsub!("hoge", "piyo")
# => FrozenError: can't modify frozen String
またはファイル先頭に以下を記載すれば良い
# frozen_string_literal: true
これにより、そのファイル内のすべての文字列リテラルが凍結される
まとめ
| 状況 | 挙動 |
|---|---|
| 定数に再代入 | 警告が出る |
| 定数オブジェクトを破壊的に変更 | 警告なし(Rubyの仕様) |
.freeze や # frozen_string_literal: true
|
FrozenErrorが発生 |
補足:検出したい場合
プロジェクト内で定数の破壊的変更を防ぎたいなら、RuboCopの以下ルールを有効にするのがおすすめとのこと
Style/MutableConstant:
Enabled: true
最後に
Rubyでは「定数」という名前でも中身のオブジェクトまで不変とは限らないことを学びました。
不変にしたいなら freeze する、または # frozen_string_literal: true を活用するのが良いみたいです。
引き続き、学習を進めていきます。
間違えていたら、すいません。