2
2

More than 3 years have passed since last update.

Rubyの定数について

Posted at

概要

Rubyの定数の仕様が他のプログラミング言語と異なるという話は有名です。

定数はオブジェクトを割り当てる"ラベルのようなもの"です。
オブジェクト(数値リテラル等を含む)=定数そのものではない、ということですね。

本記事はコンテナオブジェクトの凍結についてのTipsを主にしています。

本題

例えばC言語で定数を定義し、それを変更してみるとこうなります。

sample.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でも定義してみましょう。

sample.rb

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とか、固定しておきたいものは変更しなくてもいいですよね。

配列オブジェクトなどのコンテナに要素を追加、要素を削除するといった挙動を再現したい場合には、コンテナオブジェクトを凍結させてみましょう。

sample.rb
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という 別の方法を使う。

RHGより引用しました。

凍結の落とし穴

さて、オブジェクトを凍結させる方法を学んだ私達ですが、
実は先程の凍結方法には、1つの落とし穴があります。

次のコードを見てみましょう。

sample.rb
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つです。

sample.rb

module Defaults
    NETWORKS = ["192.168.1","192.168.2"].map!(&:freeze).freeze
end

自分も最初何をやっているのか全くわからなかったですが、

map(&:メソッド名)で、各要素にメソッドを適用することが出来るみたいです。

Rubyではやたら使う記法だそうですし、実際かっこいい。

前半のmap!メソッドで各要素をフリーズして、その後のメソッドチェーンで
配列自体をフリーズしている、ということです。

これで、先程のコードを実行した時にエラーが出ます。

sample.rb
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.%記法を使う

sample.rb
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に実際に記載されているので、現在進行形でこの本の勉強をしている方の為に残しておきますが、コメント欄をご一読ください。

さて、凍結について理解を深めた私達は、
もう怖いものはありません、とばかりに次のコードを書きました。

sample.rb
NUMBER = 10
NUMBER.freeze

p NUMBER.frozen?                     #=>true

frozen?メソッドは、そのオブジェクトが凍結しているか判定して真偽値を返します。わかりやすい。

凍結すれば不変的なオブジェクトというわけですから、

当然次のようなコードはエラーを起こすはずです。

sample.rb
NUMBER = 10
NUMBER.freeze

NUMBER = 50        
p NUMBER                        #=> 50

WHY JAPANESE PEOPLE!?

はい、最大の混乱ポイントの登場です。

最初の、

sample.rb

TEISUU = 30
TEISUU = 25
puts "できたよ" if TEISUU == 25

このコードのような形ですね。

Rubyでは、
既存の定数に新しい値を代入しても文法違反にはなりません。

EffectiveRubyでも「この方法は不格好で状況によっては面倒すぎるが単純である。
と述べられています。

解決策はこちら。

sample.rb
module Namespace
    NUMBER = 10
end

Namespace.freeze

Namespace::NUMBER = 50       #=>  FrozenError

簡単ですね。

モジュールやクラスで定数をラッピングして、それごと凍結させる。

場合によっては定数を格納する専用のクラスやモジュールを定義して、
freezeする手間を省く事を検討してもいいのだとか。

注意!

こちらの記事で述べたことを思い出してください。

クラスパスセパレータを記述せずNUMBERに50を代入すると、
Namespace内の定数とは別のスコープに新たな定数NUMBERが定義されます!

これは間違えやすいミスなので気をつけて下さいね。

最後に

Rubyの定数とオブジェクトの凍結について解説しました。

EffectiveRuby挑戦中の皆様一緒に頑張りましょうね。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2