Ruby の特異クラスの“振る舞い”のナゾ


前回の、Smalltalk の世界観に影響を受けた言語ではクラスもオブジェクトであらんとする…的な話の続きのような、そうでないような(^_^;)。


Java のクラス同様、Ruby のクラスもオブジェクトで、すべてのクラスは(やはり Java と同じ…)「Class」という名のクラスのインスタンスです。しかし、Ruby のクラスは Java のクラスよりオブジェクトとしては個性豊かで、クラス特異的な振る舞い“クラスメソッド”(Java の static 的なものではなく、Smalltalk でいうところの…)や属性“クラスインスタンス変数”(クラス変数や static 変数とは別に…)を定義して利用できます。これは、特異クラスという Java にはない特殊な仕組みにより実現されています。


特異クラスというのは、Ruby のオブジェクト(クラスに限らず…)において、インスタンス特異的なメソッド(特異メソッド)を定義するために便宜的に作られる匿名クラスのことです(特異クラス、特異メソッドともに Ruby ローカルな用語)。特異クラスは、もともと C 言語レベルでしか存在させないオブジェクトとして考えられていたせいか、通常の方法、たとえば、オブジェクトへの class メッセージの送信ではアクセスできないようになっています。


しかし、次のような特異クラスの定義式をイディオムとして用いることで、あるオブジェクト(obj)の特異クラスへのアクセスが可能になります。

class << obj; self end


これに適当な名前をつけて Object クラスのメソッドとして定義しておけば、クラスを含む任意のオブジェクトの特異クラスにアクセスする手段を得ることができます。

class Object
  def singleton_class
    class << self; self end
  end
end
Object.class             #=> Class
String.class             #=> Class
Class.class              #=> Class
Object.singleton_class   #=> #<Class:Object>
String.singleton_class   #=> #<Class:String>
Class.singleton_class    #=> #<Class:Class>


ただ、もともとが特異クラスの定義のための式であるという性格上、このメソッドには、特異クラスがないときにはそれを新たに生じさせてしまう…という副作用(?)が想定されます。通常のクラスには対応する特異クラスが必ず存在するので問題はないのですが、それ以外のオブジェクト(特異クラスを含む)では、観察したい状況に望まざる変化を生じさせてしまう可能性があるので注意が必要です。


ところで、Rubyソースコード完全解説 - 第4章 クラスとモジュール によれば、クラスの特異クラスは自身のクラスでもあるようです。このことが本当なら、自身に定義されたメソッドを自分に属するクラスからだけでなく、自らも起動できるはずです。

#通常のクラスへのメソッド定義では…
class Foo; def m0; end end   # メソッド「Foo#m0」の定義
Foo.instance_methods(false).include?("m0")   #=> true
Foo.new.m0                   #=> nil -- 当然、起動できる。
Foo.m0                       #=> NoNameError -- 当然、起動できない。
#特異クラスへのメソッド定義では…
def Class.m1; end          # Class クラスの特異クラスへのメソッド「m1」の定義
Class.singleton_class.instance_methods(false).include?("m1")  #=> true
Class.m1                   #=> nil -- 当然、起動できる。
Class.singleton_class.m1   #=> nil -- 自身からも起動できる!

たしかにそう(クラスの特異クラスは自身のクラスでもある→自身に定義されたメソッドを起動できる…)なっています。…といいたいところですが、なぜだか、この振る舞いは例外的に Class クラスの特異クラス(#)でしか確認できていません(仕様?バグ?)。他のクラスの特異クラスで同じことを試してもメソッドが見つからずエラーになります。

def Foo.m2; end  # Foo の特異クラスへのメソッド「m2」の定義
Foo.singleton_class.instance_methods(false).include?("m2")   #=> true
Foo.m2                  #=> nil -- これは問題なし。
Foo.singleton_class.m2  #=> NoNameError -- !?


しかし、クラスの特異クラスの特異クラスについては、想定されるとおり自身に定義されたメソッドを起動できる…という不思議も。

def (Foo.singleton_class).m3; end
Foo.singleton_class.m3                   #=> nil -- 当然、起動できる。
Foo.singleton_class.singleton_class.m3   #=> nil -- !?


ところで、この m3 が、クラスの特異クラスの特異クラスに定義されたものであり、そのまた特異クラスに定義されたものではない(すなわち、間違いなく自身に定義されたメソッドを起動している)…ということを念のため確認しようとすると…、

Foo.singleton_class.singleton_class.m3   #=> nil
Foo.singleton_class.singleton_class.instance_methods(false).include?("m3")   #=> true
Foo.singleton_class.singleton_class.singleton_class.instance_methods(false).include?("m3")   #=> false
Foo.singleton_class.singleton_class.m3   #=> NoNameError -- なにーっ!?


なんと、ついさっきまで起動できていた m3 がエラーになってしまいます。どうやら、先に述べた「副作用」による“状況の望まざる変化”が生じてしまっているようです。