ええっ!?Rubyの定数って自由に書き換えられるんですか?


Rubyでは、全てが大文字の変数は定数を表します。

定数とは、変更することが出来ず、再代入もできない、という性質を持つはずです。

試してみる

irbで試してみましょう

irb(main):001:0> STATIC='hoge'
=> "hoge"
irb(main):002:0> STATIC
=> "hoge"
irb(main):005:0> STATIC.reverse! #String#reverse!は元の文字列を破壊するメソッド
=> "egoh" # 破壊された...
irb(main):007:0> STATIC='fuga' # 代入してみる
(irb):7: warning: already initialized constant STATIC
(irb):1: warning: previous definition of STATIC was here
=> "fuga" #警告は出るけど代入されてしまった

なんと。再代入も変更もできてしまいました。

実際には、「大文字で書かれた変数は定数だから参照専用として取り扱ったコードを書け」というのは暗黙の了解になっている気がしますが、それでもプログラマの良心に依存した、仕組みとしては不完全なつくりになっていますね。

どうするか

Object#freezeを使う

Object.freezeは、そのレシーバへの破壊的な操作に対して例外を発生させ、処理をストップさせるようにできるメソッドです。

結果

irb(main):001:0> STATIC = 'hoge'.freeze
=> "hoge"
irb(main):002:0> STATIC.reverse!
RuntimeError: can't modify frozen String
	from (irb):2:in `reverse!'
	from (irb):2
	from /Users/yukito/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'
irb(main):003:0> STATIC='fuga'
(irb):3: warning: already initialized constant STATIC
(irb):1: warning: previous definition of STATIC was here
=> "fuga"

破壊的なメソッドにはエラーを返すようになりましたが、これでも再代入はできてしまいます。

それでは再代入を防ぐ方法はあるのか?

moduleに突っ込んで両方freezeする

定数をConstモジュールに閉じ込めて、モジュールとその中の定数両方にfreezeをかけます。

irb(main):001:0> module Const
irb(main):002:1>   STATIC='hoge'.freeze
irb(main):003:1> end
=> "hoge"
irb(main):004:0> Const.freeze
=> Const
irb(main):005:0> Const::STATIC='fuga'
RuntimeError: can't modify frozen Module
	from (irb):5
	from /Users/yukito/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'
irb(main):006:0> Const::STATIC.reverse!
RuntimeError: can't modify frozen String
	from (irb):6:in `reverse!'
	from (irb):6
	from /Users/yukito/.rbenv/versions/2.4.1/bin/irb:11:in `<main>'
irb(main):007:0>

この方法なら、無事定数への破壊行動(?)を阻止することができました。

普段Railsを触っているとなかなか気づきにくい穴ではありますが、Effective Rubyでは、このようにRuby言語仕様のワナや、それを踏まえたベストプラクティスが提示されています。

Ruby言語そのものに対する理解をさらに深めたいという方は、勉強してみても良いのではないでしょうか。