トップ «前の日記(2006年08月14日) 最新 次の日記(2006年09月04日)» 編集

ohai日誌

2003|12|
2004|01|02|03|04|05|06|07|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|06|08|10|11|
2008|01|02|03|04|05|07|09|
2009|01|02|
2013|06|12|
2014|01|02|03|04|06|09|10|12|
2015|04|
2016|09|
2018|02|

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を見てください。


トップ «前の日記(2006年08月14日) 最新 次の日記(2006年09月04日)» 編集