Rubyのメソッド呼び出し順

※以下の記述は ruby 1.9.1-p129 を前提としています。

Ruby逆引きハンドブックの「SECTION-40 呼び出されるメソッドの決定方法」(p.145)が大変参考になったので、復習を兼ねて実際に試してみました。

基本的にクラスの継承ツリーを辿っていくわけですが、Rubyの場合は特異メソッドやモジュールのMix-inなどが絡んでくるため、そのあたりの関係をちゃんと認識しておく必要があります。

インスタンスメソッド

次のようなコードを書いて実行してみます。

instance_method.rb
#!/usr/bin/env ruby
# coding: utf-8

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

module SuperIncludeModule
  def foo
    [ "親クラスでincludeしたモジュールのメソッド" ] + super
  end
end

class SuperClass
  include SuperIncludeModule

  def foo
    [ "親クラスのメソッド" ] + super
  end
end

module SubIncludeParentModule
  def foo
    [ "自クラスでincludeしたモジュールの親モジュールのメソッド" ] + super
  end
end

module SubIncludeModule
  include SubIncludeParentModule

  def foo
    [ "自クラスでincludeしたモジュールのメソッド" ] + super
  end
end

class SubClass < SuperClass
  include SubIncludeModule

  def foo
    [ "自クラスのメソッド" ] + super
  end
end

bar = SubClass.new

def bar.foo
  [ "特異メソッド" ] + super
end

module SubExtendModule
  def foo
    [ "extendしたモジュールのメソッド" ] + super
  end
end
bar.extend SubExtendModule

puts bar.foo
実行結果
$ ruby instance_method.rb 
特異メソッド
extendしたモジュールのメソッド
自クラスのメソッド
自クラスでincludeしたモジュールのメソッド
自クラスでincludeしたモジュールの親モジュールのメソッド
親クラスのメソッド
親クラスでincludeしたモジュールのメソッド
Objectクラスのメソッド

includeextend はメソッド定義を上書きするわけではなく、継承ツリーの合間に挟まっていく形になるのだということがわかります。

ちなみに、Rubyではトップレベルで定義された関数は、Objectクラスのプライベートメソッド*1となりますので、上のコードの

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

この部分を

def foo
  [ "トップレベルのメソッド" ]
end

こう書き換えると、

$ ruby instance_method.rb 
特異メソッド
extendしたモジュールのメソッド
自クラスのメソッド
自クラスでincludeしたモジュールのメソッド
自クラスでincludeしたモジュールの親モジュールのメソッド
親クラスのメソッド
親クラスでincludeしたモジュールのメソッド
トップレベルのメソッド

こんな感じの実行結果になります。

クラスメソッド

クラスメソッドの場合はもう少し複雑になります。細かな解説はRuby逆引きハンドブックのp.147あたりを読んで頂くとして、実際にコードを書いて実行してみます。

class_method.rb
#!/usr/bin/env ruby
# coding: utf-8

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

class Module
  def foo
    [ "Moduleクラスのメソッド" ] + super
  end
end

class Class
  def foo
    [ "Classクラスのメソッド" ] + super
  end
end

class SuperClass
  def self.foo
    [ "親クラスのクラスメソッド" ] + super
  end
end

class SubClass < SuperClass
  def self.foo
    [ "自クラスのクラスメソッド" ] + super
  end
end

module SuperExtendParentModule
  def foo
    [ "親クラスをextendしたモジュールの親モジュールのメソッド" ] + super
  end
end

module SuperExtendModule
  include SuperExtendParentModule

  def foo
    [ "親クラスをextendしたモジュールのメソッド" ] + super
  end
end
SuperClass.extend SuperExtendModule

module SubExtendModule
  def foo
    [ "自クラスをextendしたモジュールのメソッド" ] + super
  end
end
SubClass.extend SubExtendModule

puts SubClass.foo
実行結果
$ ruby class_method.rb 
自クラスのクラスメソッド
自クラスをextendしたモジュールのメソッド
親クラスのクラスメソッド
親クラスをextendしたモジュールのメソッド
親クラスをextendしたモジュールの親モジュールのメソッド
Classクラスのメソッド
Moduleクラスのメソッド
Objectクラスのメソッド

ここで注目したいのは SuperClass.extendSuperClass 及び SubClass の定義の後に実行している部分で、ここからも「継承ツリーに継ぎ足していく形になるので定義の順番は関係ない」のだということがわかります。

複雑な例

以下のようなコードを考えてみます。

complex_mixin.rb
#!/usr/bin/env ruby
# coding: utf-8

def foo
  [ "トップレベル" ]
end

module ParentModule
  def foo
    [ "親モジュール" ] + super
  end
end

class SuperClass
  include ParentModule

  def foo
    [ "親クラス" ] + super
  end
end

module ChildModule
  include ParentModule

  def foo
    [ "子モジュール" ] + super
  end
end

class SubClass < SuperClass
  include ParentModule
  include ChildModule

  def foo
    [ "自クラス" ] + super
  end
end

bar = SubClass.new

bar.extend ParentModule

puts bar.foo

ParentModule が、

  1. SuperClass でincludeされている
  2. ChildModule でincludeされている
  3. SubClass でincludeされている
  4. SubClassのインスタンス をextendしている

と、都合4回使われています。この場合 "親モジュール" がどの時点で表れるのか、一瞬考え込んでしまいますが、実行結果は

$ ruby complex_mixin.rb 
自クラス
子モジュール
親クラス
親モジュール
トップレベル

と、特におかしなところもない、自然な結果になってくれます。

Module#includeObject#extend が、同じモジュールが指定された場合は2回目以降を無視してくれるためですね。

Module#ancestors

Module#ancestors メソッドを使うと、上述のメソッド探索順を簡単に確認することができます。

例えば、先ほどの instance_method.rb の末尾、

puts bar.foo

puts bar.class.ancestors

と書き換えて実行すると、

$ ruby instance_method.rb 
SubClass
SubIncludeModule
SubIncludeParentModule
SuperClass
SuperIncludeModule
Object
Kernel
BasicObject

と、大体似たような結果を得ることができます。

ただし、見てわかる通り、ここには 特異クラス は表示されないため、完全な探索経路が得られるわけではないようです。

Ruby逆引きハンドブック

Ruby逆引きハンドブック

*1:レシーバを指定できない=関数形式でしか呼び出せないメソッド