2006年08月20日
_ クラス階層関連
今回は特異クラスとクラスの継承関係についてです。 予告と内容が違いますがご容赦を。
参考文献はRHGの第4章。 今回の話もRubyのソースコードを見たほうがわかりやすいかもしれません。
クラスと特異クラス
Rubyのすべてのオブジェクトは何らかのクラスのインスタンスです。
class A < Object
def foo
puts "foo"
end
end
a = A.new
とすると、aが指すオブジェクトはクラスAのインスンタンスです。 つまりRubyの任意のオブジェクトはそれ自身が属するクラスの 情報を持っていなければなりません。これを取りだすのが Object#classです。
さて、Rubyには特異メソッドというある1つのオブジェクトのみが 利用できるメソッドがあります。
def a.bar puts "bar" end
とすると、aが指すオブジェクト固有のメソッドが定義されます。 しかし、Rubyではメソッドはクラス/モジュールに対し定義 されるはずのものです。つまりaが指すオブジェクト固有の クラスが必要になります。これが特異クラスです。 実際の実装としては、特異メソッド定義時に Aをスーパークラスとする新しいクラスを作成して aのクラスをその新しく作ったクラスに変更します。
しかし、こうしてもa.classはAを返します。 Object#classは特異クラスをスキップするようになっているからです。 Rubyの思想として特異クラスはあまり使うべきでないと考えられていて、 特異クラスはわざと使いにくくなっています。 ただそうはいっても何らかアクセス方法がないと不便な場合があります。 そのため、特異クラス構文と呼ばれるものが(裏道的なものとして) あります。
class << a : end
とすることでaが指すオブジェクトの特異クラスに直接触れることができます。 よって、
class Object
def singleton_class
class << self
self
end
end
end
とObject#singleton_classを定義すれば、特異クラスを直接取りだすこと もできるようになります。
includeとextend
Module#includeとObject#extendの実装は実はおよそ以下の通りになっています。 リファレンスマニュアルのModule#append_featuresの項も参考にしてください。
class Module
def include(*modules)
raise if modules.any?{|mod| mod.instance_of?(Module)}
modules.reverse_each |mod|
mod.append_features(self)
mod.included(self)
end
end
def append_features(class_or_mod)
include_module(class_or_mod)
end
def included(class_or_mod)
end
def extend_object(object)
include_module(object.singleton_method)
end
def extended(object)
end
# includeの本体、rb_include_module
# 実際にはこのメソッドはRubyから直接触れることはできない
def include_module(class_or_mod)
class_or_modの継承チェインにselfとselfにincludeされている
モジュールをを追加する
ただしすでにincludeされているものは無視される
end
end
class Object
def extend(*modules)
raise if modules.any?{|mod| mod.instance_of?(Module)}
modules.reverse_each |mod|
mod.extend_object(self)
mod.extended(self)
end
end
end
このようになっているため、あるモジュールのappend_featuresや extend_objectをオーバーライドすることで、includeやextendの操作を 根本的に変更してしまうことができます。例えば、
module A def self.append_features(class_or_mod) end end
とすれば、このモジュールはincludeしても何も起きないモジュール になります。
module A
def self.append_features(class_or_mod)
raise "module A cannot be included"
end
end
とするとincludeした瞬間に例外が発生するようになります。 これによって「includeできないモジュール」を作ることができます。
また、includedやextendexのほうをオーバーライドすることで、 includeの動作に+αの機能を加えることができます。 includeの動作を根本的に変更したい場合はあまりないでしょうから 通常はこちらを使うことが多いでしょう。
一例として、Singletonモジュールの簡易的な実装を見てみましょう。 マルチスレッドの問題やエラーチェック等はとりあえず無視します。
module Singleton
# Singletonオブジェクトがcloneやdupで複製できるのはおかしいので
# 定義を上書きする。
def clone
raise "can't clone"
end
def dup
raise "can't dup"
end
# includeしたときに呼びだされる。
# klassはインクルードするクラス
def self.included(klass)
class <<klass
# newを外から呼びだせなくする
private :new
# Singletonオブジェクトを保管しておく変数
_instance = nil
# klass.instanceを定義する
define_method(:instance) do
# 最初にこのメソッドが呼ばれたときにはnewを呼びだして
# インスタンスを生成する。二回目以降は生成したオブジェクトを
# そのまま返す。
_instance ||= new
end
end
end
end
とりあえずこれだけです。
class A include Singleton end
とすると、 A.new でエラー(NoMethodError: private method `new' called for A:Class) が出、A.instanceでインスタンスが得られます。 A.instanceは何度呼んでも同じオブジェクトが得られます。これは 実際にirbなどで試してください。
また、別の例として、モジュールをincludeしたときにクラスメソッドも 定義されるようにする方法を示します。 Rubyではクラスメソッドを継承するために継承時にクラスの特異クラス間に 特別な継承関係を作っています(詳しくは上の参考資料を見てください)。 しかしモジュールに関してはそのような特殊なことはしていません。 そのためincludeではクラスメソッドは継承されません。そこで なんらかの細工をする必要があります。 一つの方法として、 定義したいクラスメソッドを別モジュールに定義して includeしたときにそれをextendするというやりかたがあります。 これはRailsで使われている方法です。 おおよそ以下の通りにすれば良いです。
module A
def foo
p "foo"
end
def self.included(klass)
klass.extend(A_ClassMethods)
end
module A_ClassMethods
def bar
p "bar"
end
end
end
これで、
class B include A end
とすると
B.new.foo B.bar
などとできます。
注意として、通同ではじmoduleを2回includeしても2度目以降は何も起きないのですが、 includedやappend_featuresをオーバーロードした場合これが成立しなくなります。
以上はモジュールをincludeする話でしたが、その他にも情報収集のためのメソッド として Module#ancestors, Module#included_modules, Module#include?などが あります。ancestorsは継承チェインを配列で得る機能です。ごちゃごちゃと includeしたり継承したりしたクラスをデバッグしたりするときに役にたつでしょうか。 あとのメソッドもおおよそ名前の通りです。 リファレンスのclass Moduleを見てください。