0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[ Ruby ] 継承ツリーについてメソッドが探索される流れを見ながら調べてみる

Last updated at Posted at 2020-11-15

#はじめに
メソッドを呼ぶ際に、そのメソッドは自身のクラス内に定義されている場合の他にも
スーパークラス内や、module、又はクラスから作成されたインスタンスに特異メソッドとして定義されているなど
様々なケースがあるかと思います。
今回は、それらのケースの、メソッドの探索の流れについて
まとめていきたいと思います。

自身のメソッド

.rb
class Klass
  def hello
    puts "Hello_from_Klass"
  end
end

klass_obj = Klass.new
klass_obj.hello
# => Hello_from_Klass

最も単純なケースかと思われます。
objオブジェクトに対してhelloメソッドが呼ばれた際、
直接のクラスであるKlassクラスを参照し、そこにhelloメソッドがないかを探します。
今回の場合は、そこで見つけることが出来たので、そこで探索は終了しそのメソッドを実行します。

スクリーンショット 2020-11-14 22.35.18.png

スーパークラスのメソッド

.rb
class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
end

sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello_from_Klass

探索方向は、まず直接のクラスを参照し、そこになければ
その親クラス、なければ更にその親クラス・・・
のようにクラスの継承順序と等しくなります。
今回のケースでは、SubKlassを参照しhelloメソッドが無かったため
その親クラスであるKlassを参照し、helloメソッドが定義されていたため
探索は終了し、メソッドを実行します。

スクリーンショット 2020-11-14 22.50.48.png

特異メソッド

.rb
class Klass
  def hello
    puts "Hello from Klass"
  end
end

obj = Klass.new

def obj.hello
  puts "Hello from singleton"
end

obj.hello
# => Hello from singleton

このケースでは、直接のクラスに定義したhelloメソッドではなく
特異メソッドとして定義したhelloメソッドが呼ばれました。
つまり、特異クラスはオブジェクトのクラスより先にメソッドが探索されていることが
わかります。

スクリーンショット 2020-11-15 13.31.57.png

クラスにmixinされたメソッド

スーパークラスとmoduleに同名メソッドを定義して
サブクラスのインスタンスからそのメソッドを呼んでみます。

.rb
module HelloModule
  def hello
    puts "Hello from HelloModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include HelloModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from HelloModule

includeしたmoduleのhelloメソッドが呼ばれました。
includeされたmodulueは、includeしたクラスの親クラスとの間に
存在することになります。
この時、moduleをラップしたかのような無名クラスが作られ継承ツリーの中に埋め込まれています。
ちなみに、この無名クラスはRubyのプログラムからは触れることが出来ません。

このように無名クラスが作成され、継承ツリーに埋め込まれるため、
メソッドの探索も通常の継承が行われている時と同じルールで参照することができています。

スクリーンショット 2020-11-15 14.03.40.png

複数のmoduleをinclude

先ほどは、1つのmoduleのみをincludeさせましたが、
moduleのincludeはクラスの継承と異なり
1つのクラスに対して複数のmoduleをincludeすることが出来ます。

実際に複数のmoduleをincludeさせてメソッドを呼んでみます。

.rb
module FirstModule
  def hello
    puts "Hello from FirstModule"
  end
end

module SecondModule
  def hello
    puts "Hello from SecondModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include FirstModule
  include SecondModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from SecondModule

複数のmoduleをincludeさせた場合、
最後にincludeさせたmoduleのメソッドが呼ばれました。
このように複数のmoduleを同一のクラスにincludeさせた場合、
includeさせた順にmoduleはそのクラスの継承ツリーに埋め込まれていきます。

まず、FirstModuleがincludeされ以下の継承ツリーになります。
スクリーンショット 2020-11-15 14.18.27.png

その後、下に記述してあるSecondModuleがincludeされると、以下のようになります。
スクリーンショット 2020-11-15 14.20.11.png

moduleをincludeした後に、再度include

今度は、先ほどのようにFirstModule``SecondModuleの順に読み込んだ後に
再度FirstModuleを読み込んでみます。

.rb
 module FirstModule
  def hello
    puts "Hello from FirstModule"
  end
end

module SecondModule
  def hello
    puts "Hello from SecondModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include FirstModule
  include SecondModule
  include FirstModule
end


sub_klass_obj = SubKlass.new
sub_klass_obj.hello
# => Hello from SecondModule

最後にincludeの記述があるのはFirstModuleですが
呼ばれたのはSecondModuleのメソッドでした。

実は継承ツリーにmoduleをラップしたクラスを差し込む際に、
継承ツリーに同じmoduleがないかを確認しています。
そのため、2回目のinclude FirstModuleが呼ばれた際に、
既にFirstModuleがincludeされているため
差し込まれずにincludeメソッドが終了します。

moduleをprepend

.rb
module PrependModule
  def hello
    puts "Hello from PrependModule"
  end
end

module IncludeModule
  def hello
    puts "Hello from IncludeModule"
  end
end

class Klass
  prepend PrependModule
  include IncludeModule
  def hello
    puts "Hello from Klass"
  end
end

klass_obj = Klass.new
klass_obj.hello
# => Hello from PrependModule

includeはmoduleをラップした無名クラスを
includeしたクラスより上位に埋め込むのに対して
prependはそのクラスより下位に埋め込まれます。(prependしたクラスより優先的にメソッドが呼ばれる)
そのため、prependするmodule内でsuperの呼び出しを行うと、
prependしたクラスの同名メソッドが呼ばれます。

スクリーンショット 2020-11-15 16.51.41.png

オブジェクトに存在しないメソッドを呼び出す

.rb
module HelloModule
  def hello
    puts "Hello from FirstModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end
end

class SubKlass < Klass
  include HelloModule
end

obj = SubKlass.new
obj.bye
# => undefined method `bye' for #<SubKlass:0x00007fb5fb80c628> (NoMethodError)

オブジェクトのどこにも存在しないbyeメソッドを呼んでみたところ
NoMethodErrorを吐きました。

byeメソッドが呼ばれると継承ツリーを順に辿ってそのメソッドを探していきます。
しかし、今回はどこにも定義していないため継承ツリーの最上位であるBasicObjectまで探した後
直接のクラスであるSubKlassまで戻り、今度はmethod_missingメソッドを継承ツリーの順番に探していきます。
今回は自分でmethod_missingメソッドを定義していないため最上位である
BasicObjectmethod_missingメソッドが呼ばれNoMethodErrorを例外として発生させます。

そのため、BasicObjectまでの継承ツリーの途中でmethod_middingメソッドがあれば
それを呼び出します。

.rb
module HelloModule
  def hello
    puts "Hello from FirstModule"
  end
end

class Klass
  def hello
    puts "Hello from Klass"
  end

  def method_missing(method_name, *args)
    puts "#{method_name}?そんなメソッドないよ"
  end
end

class SubKlass < Klass
  include HelloModule
end

obj = SubKlass.new
obj.bye
# => bye?そんなメソッドないよ

まとめ

探索の流れとして
1 , レシーバの特異クラス
2 , 直接のクラス
2', moduleがincludeされていればmodule
3 , 2の親クラス
3', moduleがincludeされていればmodule
4 , 3をメソッドが見つかるまで繰り返す
5 , 継承ツリーの最上位(BasicObject)まで探索しなければ、そのメソッドでの探索を終了
6 , メソッド名を引数にして、method_missingメソッドを1から探していく
7 , なければBasicObjectmethod_missingが呼ばれNoMethodErrorの例外が発生

おわり

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?