メタプログラミングRuby - 2章 月曜日:オブジェクトモデル(その1)
前回に引き続きRubyのメタプログラミングについて学んでいきます。
オープンクラス
既存のクラスにメソッドを追加する
Rubyのクラスは標準ライブラリも含めて「オープン」であり、 あとからメソッドを追加したり、既存のメソッドを置き換えたりすることが可能である。
class String def emphasize self + '!!' end end p "Hello, world".emphasize # => "Hello, world!!"
ここでは標準ライブラリのString
に対してemphasize
を追加している。
既存のメソッドも置き換えられる
注意すべきなのは既存のメソッドを置き換えても何の警告も出ないことである。
p "Apple".length # => 5 class String def length 1 end end p "Apple".length # => 1
ここでは既存のメソッドString#length
を再定義して、1
を固定で返す実装にしている。
こうした場合でもRubyは何の警告も表示しない。
つまりこれは強力で機能であると同時にかなりのリスクも伴うことが分かる。 間違って既存のメソッドを置き換えようものなら、簡単にシステムを壊してしまう。
クラス定義は他のコードと違いはない
他の多くの言語では「クラス宣言」は専用の構文であることが多いが、 Rubyでは通常のコードと変わらない。
3.times do class Foo puts "Hello" end end
ここでは 1.ループ構文の中でクラスを定義しているし、2.クラス定義の中で特定の処理を呼び出している。
class
構文は、もしそのクラスがまだ定義されていなければそのクラスを新たに作成し、そうでなければ既存のクラスをオープンする、という挙動となっている。
オブジェクトモデル
インスタンス変数
他の多くの言語では、クラス定義に含まれるインスタンス変数は固定であるが、Rubyにおいては動的である。
class Foo def bar @x = 1 end attr_reader :x end f = Foo.new p f.x # => nil p f.instance_variables # => [] f.bar # これを呼び出すことによりインスタンス変数`@x`が追加 p f.x # => 1 p f.instance_variables # => [:@x]
ここでは@x
に値を代入するコードがFoo#bar
に定義されているが、
これを呼び出すまでは@x
は存在しない。
コード中にあるようにinstance_variables
を呼び出すことで、定義されているインスタンス変数の一覧を得ることが出来る。
メソッド
インスタンスの中身はオブジェクトによって異なるためオブジェクトごとに用意されるが、 メソッドは全オブジェクトに共通なのでクラスに定義される。
class Bar def foo puts "foo" end end b = Bar.new p b.class # => Bar p Bar.instance_methods.grep(/foo/) # => [:foo]
Bar.new
したb
オブジェクトは、クラスオブジェクトBar
への参照を持ち、
foo
はBar
クラスオブジェクトのインスタンスメソッドとして宣言されているのが分かる。
実際に呼び出されるメソッドの特定には「メソッド探索」という仕組みが使われるが、 それについては後ほど。
クラスはオブジェクト
既に簡単に触れてしまったが、Rubyにおいてはクラスも単なるオブジェクトである。
p "hello".class # => String p String.class # => Class p Class.class # => Class
"hello"
はString
クラスのオブジェクトであり、String
はClass
クラスのオブジェクトである。
(Class
の先はずっとClass
のオブジェクトである)
Rubyでは#new
を呼び出してオブジェクトを初期化するが、これはClass
クラスに定義されたインスタンスメソッドである。
# `false`は継承されたメソッドを表示しないという意味 p Class.instance_methods(false) # => [:new, :allocate, :superclass]
つまり、String.new
とした場合、String
オブジェクトに実装されたnew
、より具体的にはClass#new
メソッドを呼び出していることになる。
ちなみにallocate
というメソッドは、インスタンス生成時にinitialize
を呼び出さないところがnew
と異なる。
superclass
メソッドは親クラスを取得するのに利用できる。
p Array.superclass # => Object p Object.superclass # => BasicObject p BasicObject.superclass # => nil
継承ツリーとしては以下のようになる。
Array --> Object --> BasicObject
BasicObject.superclass
がnil
であることから分かるように、
BasicObject
がRubyにおけるルートクラスである。
モジュール
Class
クラスの親クラスはModule
である。
p Class.superclass # => Module
つまり、Class is a Module
、すべてのClass
はModule
であると言える。
Class
にはnew
、allocate
、superclass
というインスタンスメソッドが定義されていることが確認できたが、
Module
との違いはこれだけである。
定数
Rubyにおいて大文字から始まる変数はすべて定数である。
すなわちString
といったクラスについても、ただの定数であると言える。
もっともRubyにおける定数には強制力がなく、 書き換えようとした時に警告が表示されるくらいであるが。
ツリー構造
定数はツリー構造を持つ。
X = "::X" module M X = "M::X" class C X = "M::C::X" end end p ::X # => "::X" p M::X # => "M::X" p M::C::X # => "M::C::X"
ここでは以下のような階層を持っている。
- Root
- X
- M
- X
- C
- X
つまり同じ定数名X
でも所属しているスコープが異なるのである。
::
先のコード中で示したように定数のパスは::
で記述する。
Y = "::Y" module M2 Y = "M2::Y" p Y # => "M2::Y" p ::Y # => "::Y" end
定数ツリーの中間にいた場合、相対パスでアクセスするため
ルートからアクセスしていることを明示する場合には先頭に::
を記述する。
ネームスペースとしての定数
あるクラスを新たに定義しようとした時、 既に同名のクラスがあると既存クラスをオープンする形になってしまう。
いわゆる名前の競合である。
それを避けるために定数ツリーが利用できる。
module Editor class String end end p String == String # => true p String == Editor::String # => false
ここでは標準ライブラリと同じ名前のString
を宣言しているが、
Editor
モジュールの内部であり定数ツリー上の位置が異なるため別のものとして扱われている。
このように定数をネームスペースとして利用することで、 名前衝突を割けたり、分かりやすい階層にクラスを配置することが出来る。