#概要
Rubyの定数の仕様が他のプログラミング言語と異なるという話は有名です。
定数はオブジェクトを割り当てる"ラベルのようなもの"です。
オブジェクト(数値リテラル等を含む)=定数そのものではない、ということですね。
本記事はコンテナオブジェクトの凍結についてのTipsを主にしています。
#本題
例えばC言語で定数を定義し、それを変更してみるとこうなります。
#include<stdio.h>
int main(void)
{
const int TEISUU = 30;
printf("%d\n",TEISUU);
TEISUU = 20;
printf("%d\n",TEISUU);
return 0;
}
$gcc sample.c
> error assignment of read-only variable 'TEISUU'
こんな感じでエラー吐きます。
そりゃ当たり前ですよね。定数って定義してますから。
さて、そんなTEISUUをRubyでも定義してみましょう。
TEISUU = 30
TEISUU = 25
puts "できたよ" if TEISUU == 25
$ruby sample.rb
>sample.rb:2: warning: already initialized constant TEISUU
>sample.rb:1: warning: previous definition of TEISUU was here
>できたよ
ふむふむ、エラー出てるなよしよし…
!?
>できたよ
なんじゃこりゃあ
Rubyは定数を変更しても警告を出すにとどまり、
実際に変更できてしまいます。自由だあああああああああああああ
警告の内容は、
二行目:
TEISUU
定数はすでに初期化してるぜ
一行目:以前のTEISUU
を定義したのはここだぜ
ですね。優しい。
Rubyではファイル実行時に’-w’オプションを指定することで警告が表示されますが、
Ruby1.8.7から定数の再代入の際にオプション無しでもエラーが出るようになりました。
ちょうど序盤の"できたよ"ってところで起きてる警告ですね。
ご指摘感謝します。
#定数の扱い方
実はそもそも、Rubyではクラスやモジュール名も定数なんですよね。
Rubyは先頭が大文字の語句を定数だと識別します。
TEISUU
も定数だし、Array
も定数です。
クラス等は変化できますよね。
あとからメソッドを追加したりもできます。
でも、特にゲームのタイトルとかURLとか、固定しておきたいものは変更しなくてもいいですよね。
配列オブジェクトなどのコンテナに要素を追加、要素を削除するといった挙動を再現したい場合には、コンテナオブジェクトを凍結させてみましょう。
module Defaults
NETWORKS = ["192.168.1","192.168.2"].freeze
end
class Klass
include Defaults
def foo
Defaults::NETWORKS << "192.168.3"
end
end
Klass.new.foo() #=>can't modify frozen array (FrozenError)
エラー内容は、
「凍結した配列は修正できないよん」です。
このように、 Rubyについてもオブジェクトを凍結させることで他言語の定数と同じような挙動を得られます。
さて、定数という言葉で騙される人が多いのだが、定数というのは「いったん 指すオブジェクトを記憶したら二度と変えない」という意味である。定数の指すオブジェクトそれ自体が変わらないわけではない。英語で言うなら、 constantよりもread onlyのほうがよりよく意図を示しているだろう (図2)。 ちなみにオブジェクト自体が変化しないよう指示するにはfreezeという 別の方法を使う。
#凍結の落とし穴
さて、オブジェクトを凍結させる方法を学んだ私達ですが、
実は先程の凍結方法には、1つの落とし穴があります。
次のコードを見てみましょう。
module Defaults
NETWORKS = ["192.168.1","192.168.2"].freeze
end
class Klass
include Defaults
def foo
Defaults::NETWORKS.each do |item|
item[3] = "hai" if item.class == String
end
p Defaults::NETWORKS
end
end
Klass.new.foo() #=> ["192hai168.1","192hai168.2"]
String
オブジェクトにインデックスを与えると、
先頭から[i]
番目の文字を指すのはご存知かと思いますが、
今回の場合は空白ですね。
なんと、
##配列オブジェクトをフリーズしても要素はフリーズされない
ということです。
私達がフリーズする動機は、
- もちろん配列
NETWORKS
に新たに要素が加えられる事も許さない - 要素それぞれのアドレスが変わる事も許したくない
からですよね。(そんな前置きなかったですけど)
しかし、このように要素は書き換わってしまいました。
これには、次のような解決策があります。
##1.map!などのメソッドで要素それぞれをフリーズしてみる。
EffectiveRubyに書いてあるやり方の内の1つです。
module Defaults
NETWORKS = ["192.168.1","192.168.2"].map!(&:freeze).freeze
end
自分も最初何をやっているのか全くわからなかったですが、
map(&:メソッド名)
で、各要素にメソッドを適用することが出来るみたいです。
Rubyではやたら使う記法だそうですし、実際かっこいい。
前半のmap!
メソッドで各要素をフリーズして、その後のメソッドチェーンで
配列自体をフリーズしている、ということです。
これで、先程のコードを実行した時にエラーが出ます。
module Defaults
NETWORKS = ["192.168.1","192.168.2"].map!(&:freeze).freeze
end
class Klass
include Defaults
def foo
Defaults::NETWORKS.each do |item|
item[3] = "hai" if item.class == String
end
p Defaults::NETWORKS
end
end
Klass.new.foo() #=> '[]=' can't modify frozen String FrozenError
##2.%記法を使う
module Default
NETWORKS = %w(192.168.1 192.168.2).map!(&:freeze).freeze
これはRuby2.1以上で実装されたものですが、
(訂正)少なくともRuby1.8.7での実装が確認されました。ご指摘感謝します。
%記法かっこよくないですか?簡潔でみやすい。
どうやらこの記法はメモリ管理に一役買っているようで、
リソースの消費を少し軽減できるとか。
すっきりしましたか?
Rubyのエラーメッセージでよく
[]=
だとか+
が出てきますが、
Rubyではこういった演算子や識別子なども立派なメソッドだからです。
Arrayクラスの公式リファレンスを見てみるとよくわかります。
インスタンスメソッドの欄に記号がずらずら並んでるわけですね。
EffectiveRubyの中でも特にディープな部分だと思う(そして私がスルーした)項目に、
演算子のオーバーライドの話があります。
sample.rb
def <=>(other)
return nil unless other.is_a?(Version)
...
end
とかいうやつです。
いつか手を出そうとは思いますが、いかんせんディープな話題過ぎてあまりついていけませんでした…
#凍結の落とし穴その2
注意・**Effective Rubyに載っているコードを基盤に、’frozen?’メソッドを使ってわかりやすくした以下の文章ですが、情報に誤りがある可能性があります。
Effective Rubyに実際に記載されているので、現在進行形でこの本の勉強をしている方の為に残しておきますが、コメント欄をご一読ください。**
さて、凍結について理解を深めた私達は、
もう怖いものはありません、とばかりに次のコードを書きました。
```ruby:sample.rb
NUMBER = 10
NUMBER.freeze
p NUMBER.frozen? #=>true
frozen?
メソッドは、そのオブジェクトが凍結しているか判定して真偽値を返します。わかりやすい。
凍結すれば不変的なオブジェクトというわけですから、
当然次のようなコードはエラーを起こすはずです。
NUMBER = 10
NUMBER.freeze
NUMBER = 50
p NUMBER #=> 50
#WHY JAPANESE PEOPLE!?
はい、最大の混乱ポイントの登場です。
最初の、
TEISUU = 30
TEISUU = 25
puts "できたよ" if TEISUU == 25
このコードのような形ですね。
Rubyでは、
既存の定数に新しい値を代入しても文法違反にはなりません。
EffectiveRubyでも「この方法は不格好で状況によっては面倒すぎるが単純である。」
と述べられています。
解決策はこちら。
module Namespace
NUMBER = 10
end
Namespace.freeze
Namespace::NUMBER = 50 #=> FrozenError
簡単ですね。
モジュールやクラスで定数をラッピングして、それごと凍結させる。
場合によっては定数を格納する専用のクラスやモジュールを定義して、
freezeする手間を省く事を検討してもいいのだとか。
注意!
こちらの記事で述べたことを思い出してください。
クラスパスセパレータを記述せずNUMBER
に50を代入すると、
Namespace
内の定数とは別のスコープに新たな定数NUMBER
が定義されます!
これは間違えやすいミスなので気をつけて下さいね。
#最後に
Rubyの定数とオブジェクトの凍結について解説しました。
EffectiveRuby挑戦中の皆様一緒に頑張りましょうね。