こんにちは、食べログでwebサービス開発している@tayu1605です。
去年入社し早くもエンジニア歴(Ruby歴) 2年を迎えます。
そこで今回はRubyで重要な要素である「メソッド探索」を整理していこうと思います。
#継承関係のみのクラス
まずは基本的な 継承関係のみのクラスでのインスタンスメソッドが実行される際のメソッド探索の挙動を見ていきます。
まず2つのクラスを定義します
class Hoge
def method2
"method2"
end
end
class HogeChild < Hoge
def method1
"method1"
end
end
そして、HogeChild のインスタンスで method1, method2 を実行します。
HogeChild.new.method1 # "method1"
HogeChild.new.method2 # "method2"
method1探索
- HogeChild クラスから method1 を探索
- HogeChild クラスの method1 が実行される
method2探索
- HogeChild クラスから method2 を探索
- HogeChild の親クラスである Hoge クラスから method2 を探索
- Hoge クラスの method2 が実行される
次にどこにも定義していない method3 を実行してみます。
HogeChild.new.method3 # NoMethodError
もちろん定義されてないので、NoMethodError が返ります。
ただ、ここでもメソッド探索を見ていくと、HogeChlid クラス、 Hoge クラスだけではなく、その他のクラスへも探索しにいっています。
method3探索
- HogeChild クラスから method3 を探索
- HogeChild の親クラスである Hoge クラスから method3 を探索
- Hoge の親クラスである Object クラスから method3 を探索
- Object にインクルードされたモジュールである Kernel モジュールから method3 を探索
- Object の親クラスである BasicObject クラスから method3 を探索
- BasicObject クラスに定義がないと、method_missing が実行され、 NoMethodError が返される
*Ruby ではスーパークラスを省略して記述すると、自動的に Object クラスを継承します。
そして Object クラス以降の継承関係は下記のようになっているので上のようなメソッド探索がされます。
Foo → Object → Kernel(Objectにインクルードされたモジュール) → BasicObject
#Mixin
次は Ruby で多重継承を実現する機能であるモジュールを見ていきます。
まずはモジュールを定義します。
module Fuga
def method1
"method1"
end
end
class Hoge
include Fuga
def method2
"method2"
end
end
method1を実行します
Hoge.new.method1 # "method1"
method1探索
- Hoge クラスの method1 を探索
- Hoge クラスにインクルードされた Fuga モジュールの method1 を探索
- Fuga モジュールの method1 が実行される
ここで ancestors メソッドと superclass メソッドからインクルードされたモジュールを確認してみます。
Hoge.ancestors # [Hoge, Fuga, Object, Kernel, BasicObject]
Hoge.superclass # Object
インクルードされるとインタプリタは指定されたモジュールに対応する無名クラスを作成して、スーパークラスとの間に組み入れますが、無名クラスは ancestors メソッドからは参照できるが superclass メソッドからは参照できないことがここからわかります。
#特異メソッド
次は指定されたインスタンスだけに適用される特異メソッドを見ていきます。
まずは特異メソッドを定義します。
class Hoge
end
hoge1 = Hoge.new
def hoge1.method1
"method1"
end
hoge2 = Hoge.new
特異メソッドを定義した hoge1 インスタンスと特異メソッドを定義していない hoge2 インスタンスで method1 を実行します。
hoge1.method1 # "method1"
hoge2.method1 # NoMethodError
hoge1 インスタンスの method1 探索
- 特異メソッドの method1 を探索
- 特異メソッドの method1 が実行される
hoge2 インスタンスの method1 探索
- 特異メソッドがないので、探索すらされない
- Hoge クラスに method1 を探索
・・・ - BasicObject クラスに定義がないと、method_missing が実行され、 NoMethodError が返される
methods メソッドからも hoge1 には method1 が存在するが、 hoge2 には無いことが確認できます。
hoge1.methods # [:method1, ・・・]
hoge2.methods # [・・・](method1なし)
##特異クラスに対して Mixin
次は特異クラスでincludeしてみます。
class Hoge
def method1
"method1"
end
end
module Fuga
def method2
"method2"
end
end
hoge1 = Hoge.new
class << hoge1
include Fuga
end
探索順序を追うために hoge1 インスタンスの method1 を実行してみます。
hoge1.method1 # "method1"
hoge1 インスタンスの method1 探索
- 特異クラスの method1 を探索
- 特異クラスにインクルードされた Fuga モジュールの method1 を探索
- hoge1 インスタンスのクラスである Hoge クラスの method1 を探索
- Hoge クラスの method1 が実行される
また、特異クラスへのインクルードは extend で簡単に書き換えられます。
hoge1 = Hoge.new
hoge1.extend(Fuga)
hoge1.method2 # "method2"
#prepend
prepend も include と同様にモジュールのメソッドをクラスに取り込むためのメソッドですが、同名のメソッドが存在する場合に振る舞いが異なります。
モジュールを定義して、それぞれ include と prepend するクラスを定義します。
module Fuga
def method1
"Fugaのmethod1"
end
end
class Piyo
prepend Fuga
def method1
"Piyoのmethod1"
end
end
class Hoge
include Fuga
def method1
"Hogeのmethod1"
end
end
Piyo と Hoge クラスのインスタンスで method1 を実行します。
Piyo.new.method1 # "Fugaのmethod1"
Hoge.new.method1 # "Hogeのmethod1"
Piyo の method1 探索
- Piyo クラスに prepend された Fuga モジュールから method1 を探索
- Fuga モジュールの method1 が実行される
Hoge の method1 探索
- Hoge クラスから method1 を探索
- Hoge クラスの method1 が実行される
prepend は呼び出したクラスよりも先にモジュールに対してメソッド探索されることがわかります。
また、ancestors メソッドからも確認できます。
Piyo.ancestors # [Fuga, Piyo ・・・
Hoge.ancestors # [Hoge, Fuga ・・・
#refinements
refinement は refine メソッドで変更を加えるクラスを宣言して using メソッドを呼び出すことで変更を有効にすることができます。
まずは定義します。
module Fuga
refine Hoge do
def method1
"using後のmethod1"
end
end
end
class Hoge
def method1
"method1"
end
using Fuga
def call_method1_after_using
method1
end
end
Hoge の method1 を using メソッドを呼び出す前後の挙動を見てみます。
hoge1 = Hoge.new
hoge1.method1 # "method1"
hoge1.call_method1_after_using # "using後のmethod1"
using メソッドを呼び出す前は method1 は Fuga の定義が有効になっていないので、"method1"になっています。
そして、呼び出し後、変更が有効になり、call_method1_after_using は "using後のmethod1" になっていることがわかります。
#まとめ
いかがでしたでしょうか、こちらさえ把握しておけば、どこのメソッドが呼び出されているか困ることもなくなるでしょう!
明日は @mochikichi321 による 「Bundlerを使ったGem管理について」 です!
よろしくお願いします!