My Favorite Things - Coding or die.

とある技術者の経験記録、的な。

メタプログラミング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への参照を持ち、 fooBarクラスオブジェクトのインスタンスメソッドとして宣言されているのが分かる。

実際に呼び出されるメソッドの特定には「メソッド探索」という仕組みが使われるが、 それについては後ほど。

クラスはオブジェクト

既に簡単に触れてしまったが、Rubyにおいてはクラスも単なるオブジェクトである。

p "hello".class # => String
p String.class  # => Class
p Class.class   # => Class

"hello"Stringクラスのオブジェクトであり、StringClassクラスのオブジェクトである。 (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.superclassnilであることから分かるように、 BasicObjectRubyにおけるルートクラスである。

モジュール

Classクラスの親クラスはModuleである。

p Class.superclass  # => Module

つまり、Class is a Module、すべてのClassModuleであると言える。

Classにはnewallocatesuperclass というインスタンスメソッドが定義されていることが確認できたが、 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モジュールの内部であり定数ツリー上の位置が異なるため別のものとして扱われている。

このように定数をネームスペースとして利用することで、 名前衝突を割けたり、分かりやすい階層にクラスを配置することが出来る。

今回のまとめ

  • Rubyオープンクラスである
  • Rubyにおいてクラス定義に紐付いたインスタンス変数というものは無い(動的)
  • RubyのクラスはClassクラスのオブジェクトである
  • Classクラスの親クラスはModuleである
  • ClassクラスはModuleに加えてnewallocatesuperclassを定義したクラスである
  • RubyのルートクラスはBasicObjectである
  • Rubyのクラスは単なる定数である
  • 定数はツリー構造を持つ