まず前提として、この記事を書いているのは未経験からプログラミングに挑戦中の初学者になりますのでご注意ください。 学習はフィヨルドブートキャンプに参加しながら取り組んでいて、現役のエンジニアの方からレビューをもらいながら進めています。 現在の学習状況は以下を随時更新しているので興味ある方はご覧ください。 学習を進めていく中で定数を扱うタイミングがありました。 「定数」と文字だけみると変更できない印象を持ちますが、Rubyにおける定数は警告はでるものの、変更ができてしまうため、変更されたくない場合は対策が必要になります。 特に再代入できるだけでなくミュータブルなオブジェクト(文字列・配列・ハッシュなど)であれば定数の値を変更できてしまうので注意しなければならないことを学びました。 この記事では定数が変更できてしまう点について自分でコードを打っていろいろと確認してみたので、自己学習の記録も兼ねてアウトプットしている記事になります。 結論は「freezeメソッドを使ってオブジェクトを凍結する」になります。凍結されたオブジェクトを変更しようとすると例外 FrozenError を発生させることができます。 ただし、配列やハッシュを使う場合は使い方に注意が必要だと感じました。理由はこれ以降の対策の部分で解説したいと思います。 理由を検索していたらRubyを作った松本行弘氏がQuoraの質問で以下のように回答で以下のように解説していました。 Q.なぜRubyでは定数も再代入可能で、わざわざfreezeを使わないとだめになっているのですか?ハナから再代入不可にしなかった理由は? この質問は、定数という言葉が示すと考えられるふたつの役割が混同されています。そして、Rubyではこのふたつの役割は明確に異なるものです。 第一の役割は、参照している値(オブジェクト)が変化しない、というものです。Rubyの定数はこの役割だけを持ち、大文字から始まる名前は定数として、その参照先を変えないことになっています。質問のうち、「再代入可能で、ハナから再代入不可にしなかった」というのはこの役割を示しています。 これは確かにその通りで、Rubyでは定数に再代入すると、警告こそ出力されますが、それを無視さえすれば、定数の値を書き換えることができます。 なぜそうなっているかというと、Rubyをアプリ組込みに利用して、たとえばRubyを組み込んだエディタを開発した場合、Rubyで記述した設定ファイルに書いてある定数が書換不能でエラーになってしまった場合、設定ファイルとしての使い勝手が悪くなることが想定されたからです。そこで、定数であるにも関わらず、書き換え禁止は警告のみにとどめ、やろうと思えば書き換えられるが紳士協定として、定数とするという方針にしました。 定数のもうひとつの意味は、「参照先が書き換わらない」という意味です。Cのconstはこちらの意味も持っていますが、多くのオブジェクトが書換え可能(mutable)なRubyでは、定数にはこの役割は与えられていません。定数の参照先のオブジェクトが書き換わるのを禁止したければ、「わざわざfreezeを使わないとだめ」です。これはRubyの「定数」とは無関係です 定数には 1.「参照している値(オブジェクト)が変化しない」 2.「参照先が書き換わらない」 の2つの役割があり、Rubyの定数は「参照している値(オブジェクト)が変化しない役割」だけを持っているため再代入や変更ができると説明されていました。 なるほど。難しいけど興味深い…。 実行してみると再代入後の値が返って変更ができていることが確認できます。 クラス外からも変更できました。 クラス自体をfreezeすると変更を防止できる しかし、私が学習しているフィヨルドブートキャンプでメンターもしていて、有名なチェリー本を書いている伊藤さんによれば「Rubyの場合、普通は定数を上書きする人はいないためクラスをfreezeまでする例は少ない」とのことです。 しかし、変更できるという点は知っておく必要はありそうです。 定数に配列を入れた場合で実際にコードを入力してみると以下のように変更できることが確認できました。 追加もできる 削除もできる freezeメソッドを使って凍結すると? freezeを使って凍結すると変更に対してFrozenErrorが返ってくるようになる。 しかし、この状態ではまだ完全に変更を防ぐことができているわけではないことに注意! 配列やハッシュをfreezeしても配列・ハッシュそのものは凍結できても各要素については凍結されていない。 このように変更できることがわかります。要素に対する変更も防ぎたい場合は以下のように、各要素に対してもfreezeする必要があります! 追加できる 削除もできる 変更もできる freezeすると凍結できるが要素までは防げない ハッシュ自体は凍結されている 要素に対しての変更はできてしまう ハッシュの場合はeachしても長ったらしいですし、mapは配列の戻り値になっていまいます。以下のように要素ごとにfreezeするのもなんか不恰好。 Hash#transform_values (Ruby 3.2 リファレンスマニュアル) 以上、Rubyの定数について深掘りしてみました。 今回調べてみて定数には 「参照している値(オブジェクト)が変化しない」 「参照先が書き換わらない」 という2つの役割がそもそもあり、Rubyの定数は 「参照している値(オブジェクト)が変化しない役割 だけを持っているため、再代入や変更ができる」 という理由を確認することができました。 またその対策に
はじめに
前提
Rubyの定数は書き換えができてしまうので注意が必要
結論:freezeメソッドでオブジェクトを凍結(内容の変更を禁止)する
そもそもなぜRubyの定数は変更ができてしまうのか調べてみた
定数の変更ができてしまう事例と対策
クラスをfreezeして変更を防ぐ事例
class Cafe
COFFE_PRICE = 300
COFFE_PRICE = 500
end
p Cafe::COFFE_PRICE
#=> warning: already initialized constant Cafe::COFFE_PRICE
#=> warning: previous definition of COFFE_PRICE was here
#=> 500
class Cafe
COFFE_PRICE = 300
end
Cafe::COFFE_PRICE = 500
p Cafe::COFFE_PRICE
#=> warning: already initialized constant Cafe::COFFE_PRICE
#=> warning: previous definition of COFFE_PRICE was here
#=> 500
class Cafe
COFFE_PRICE = 300
end
Cafe.freeze
Cafe::COFFE_PRICE = 500
p Cafe::COFFE_PRICE #=> can't modify frozen #<Class:Cafe>: Cafe (FrozenError)
定数(配列)が変更されてしまう例と対策
MEMBER = ['Foo', 'Bar', 'Baz']
MEMBER[0].upcase!
p MEMBER #=> ["FOO", "Bar", "Baz"]
MEMBER = ['Foo', 'Bar', 'Baz']
MEMBER.push('Piyo')
p MEMBER #=> ["Foo", "Bar", "Baz", "Piyo"]
MEMBER = ['Foo', 'Bar', 'Baz']
MEMBER.pop
p MEMBER #=> ["Foo", "Bar"]
MEMBER = ['Foo', 'Bar', 'Baz'].freeze
MEMBER.push('Piyo')
p MEMBER #=> can't modify frozen Array: ["Foo", "Bar", "Baz"] (FrozenError)
MEMBER = ['Foo', 'Bar', 'Baz'].freeze
MEMBER[0].upcase!
p MEMBER #=> ["FOO", "Bar", "Baz"]
MEMBER = ['Foo', 'Bar', 'Baz'].map(&:freeze).freeze
MEMBER[0].upcase!
p MEMBER #=> can't modify frozen String: "Foo" (FrozenError)
定数(ハッシュ)が変更されてしまう例と対策
FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}
FILE_TYPE.store('Foo', 'Bar')
p FILE_TYPE #=> {"file"=>"-", "directory"=>"d", "link"=>"l", "Foo"=>"Bar"}
FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}
FILE_TYPE.delete('link')
p FILE_TYPE #=> {"file"=>"-", "directory"=>"d"}
FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}
FILE_TYPE['link'].upcase!
p FILE_TYPE #=> {"file"=>"-", "directory"=>"d", "link"=>"L"}
FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}.freeze
FILE_TYPE.delete('file')
p FILE_TYPE #=> can't modify frozen Hash: {"file"=>"-", "directory"=>"d", "link"=>"l"} (FrozenError)
FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}.freeze
FILE_TYPE['link'].upcase!
p FILE_TYPE #=> {"file"=>"-", "directory"=>"d", "link"=>"L"}
FILE_TYPE = {
'file' => '-'.freeze,
'directory' => 'd'.freeze,
'link' => 'l'.freeze
}.freeze
FILE_TYPE['link'].upcase!
p FILE_TYPE #=> can't modify frozen String: "l" (FrozenError)
transform_values
という便利なメソッドを発見したので以下のようにもできました。transform_values
はすべての値に対してブロックを呼び出した結果で置き換えたハッシュを返します。キーは変化しません。FILE_TYPE = {
'file' => '-',
'directory' => 'd',
'link' => 'l'
}.transform_values(&:freeze).freeze
FILE_TYPE['link'].upcase!
p FILE_TYPE #=> can't modify frozen String: "l" (FrozenError)
最後に
freeze
メソッドを使う方法があるが、ハッシュや配列に対してfreezeしても各要素には凍結が適用されないことにも注意が必要だと感じました。