【ruby】 メソッド探索から見る、モジュール・特異メソッド・特異クラス
rubyを書き始めて間もない頃、
「なんで NoMethodError
なんだ...。あ、メソッド定義にself
付けたら通った。」
みたいなことがありました。
rubyの本を読んでいると、そのあたりがハッキリとイメージできるようになったのでまとめておきます。
参考にした本
年明けからひたすらRuby本を読んでいます。読了したのは以下。
現在は Effective Ruby を読んでいます。
これらを読んでいくと、中途半端に理解していた部分がカチッとハマるのでオススメです。
※ 今回のコードは ruby 2.2.0
で試したものです。
オブジェクトとクラスの関係
サンプルコードを見てみましょう。
class C def c_instance_method @my_var = 1 end end obj = C.new
当初、自分はオブジェクトとクラスの関係を以下のように考えていました。 (図は本を参考にした独自のものです。クラス図とは関係ありません。)
- クラスCは雛形
- 雛形に合わせてobjオブジェクトが生成される
しかし、ruby本を読んでいると、実際はこうでした。
- オブジェクトは
インスタンス変数
とクラスへの参照
を持つ- クラスは
インスタンスメソッド
を持つ
一番驚いたのは、「オブジェクトの中にはメソッドが無い」ということ。
これはどういうカラクリかというと、objのメソッド呼び出しの時にわかります。
メソッド探索
サンプルコードでメソッドを呼び出してみましょう。
class C def c_instance_method @my_var = 1 end end obj = C.new obj.c_instance_method # ここでインスタンスメソッドを呼び出した
この時、何が起きるかというと、rubyは以下のようにメソッドを探索します。
- 参照しているクラスにメソッドを探索しにいく
- 見つかったら実行
objを2つ生成した場合はどうでしょうか。
class C def c_instance_method @my_var = 1 end end obj = C.new obj.c_instance_method obj2 = C.new # 別のオブジェクトを生成 obj2.c_instance_method # インスタンスメソッドを呼び出す
以下のようになります。
では、存在しないメソッドを呼び出した場合はどうなるのでしょうか。
class C def c_instance_method @my_var = 1 end end obj = C.new obj.missing_method # 存在しないメソッドを呼び出した
rubyは、メソッドが見つからなければsuperclassをどんどん遡って探索します。
- メソッドが見つからなければ、superclassをどんどん遡って探索
- メソッドが見つかれば、そのメソッドを実行する
- BasicObjectまで探しても見つからなければ
NoMethodError
では、今度はクラスを継承してみましょうか。
class C def c_instance_method @my_var = 1 end end class D < C; end # Cを継承したDクラスを定義 obj = D.new # Dクラスのオブジェクトを生成 obj.c_instance_method # インスタンスメソッドを呼び出す
探索は以下のようになります。
- はじめに探索するのは、オブジェクトの参照しているクラス(今回はDクラス)
- そこで見つからなければsuperclassを探索する
- 探索でメソッドが見つかれば、そこで実行する
例えばこの時、DクラスにCクラスのメソッドと同じ名前の「c_instance_method」が存在すると、探索の結果、Dクラスのメソッドが呼び出されます。これはお馴染みの「オーバーライド」です。
これで、メソッド探索の基本はOKですね。
次はもう一歩踏み込みます。
モジュール
rubyにはクラスの他にモジュールがあります。
モジュールもメソッド探索に影響を与えます。
include
モジュールをincludeした場合を見ていきます。
module M def m_method "m_method" end end class C include M # モジュールをinclude def c_instance_method @my_var = 1 end end obj = C.new obj.m_method # モジュールのメソッドを呼び出す
探索は以下のようになります。
- includeしたモジュールは、includeしたクラスのすぐ一つ上に挿入される
- superclassの探索の前にモジュールが探索される
もし、モジュールにも無いメソッドが呼び出された場合は、モジュールの次にsuperclass(上の図では Objectクラス)を探索します。
では2つのモジュールをincludeしたらどうなるでしょうか。
module M1 def m1_method "m1_method" end end module M2 def m2_method "m2_method" end end class C include M1 include M2 def c_instance_method @my_var = 1 end end obj = C.new obj.m1_method # M1のメソッドを呼ぶ
探索は以下のようになります。
- 後にincludeしたモジュールが先に探索される
- つまり、後にincludeしたモジュールでオーバーライドできる
イメージが湧きましたでしょうか。
次は、もう一つモジュールを取り組む方法である prepend です。
prepend
モジュールの取り込み方法には prepend
もあります。
module M def m_method "m_method" end end class C prepend M # モジュールをprepend def c_instance_method @my_var = 1 end end obj = C.new obj.m_method # モジュールのメソッドを呼び出す
探索は以下のようになります。
- prependしたモジュールは、prependしたクラスのすぐ一つ下に挿入される
図より、prependでメソッドのオーバーライドが可能です。
では、2つのモジュールをprependしたらどうなるでしょうか。
module M1 def m1_method "m1_method" end end module M2 def m2_method "m2_method" end end class C prepend M1 prepend M2 def c_instance_method @my_var = 1 end end obj = C.new obj.m1_method # M1のメソッドを呼ぶ
探索は以下のようになります。
- 後にprependしたモジュールが先に探索される
includeと同じで、あとからprependしたモジュールが優先され、それでオーバーライドができます。
では、さらに一歩踏み込んで、特異メソッド・特異クラスにいきましょう。
特異メソッドと特異クラス
当初の自分の理解では、「特異メソッドとは、特定のオブジェクトにのみ追加したメソッドである」というものだけでした。
「メソッドはクラスに属している」のならば、特異メソッドはどのクラスに属しているのか、疑問ですよね。
「オブジェクトの参照しているクラスに追加される」のであれば、他のオブジェクトからも呼び出せてしまうのでこれは違います。
いったいどこに特異メソッドがあるのか...。
このからくりを解き明かしていきます。
class C def c_instance_method @my_var = 1 end end obj = C.new def obj.obj_singleton_method # 特異メソッドを定義 "obj_singleton_method" end obj.obj_singleton_method # 特異メソッドを呼ぶ
探索は以下のようになります。
- 実は、各オブジェクトは必ず一つの特異クラスを持っている
- 実は、メソッド探索では、特異クラスを一番最初に探索していた
- 今までは特異クラスにメソッドが見つからなかったので素通りしていた
- 特異メソッドは特異クラスに属する
obj.singleton_class.instance_methods(false) #=> [:obj_singleton_method]
- objの特異クラスのsuperclassは、オブジェクトの参照しているクラスである
obj.singleton_class.superclass #=> C
- つまり、特異クラスにメソッドが見つからなければ、次の探索対象はオブジェクトの参照クラスである
- しかし、objの参照クラスはCクラスであることに注意
obj.class #=> C
- これは
class
メソッドの仕様。特異クラスは無視する。
ちょっと複雑かもしれませんが、
- オブジェクトは必ず一つの特異クラスを持っている
- メソッド探索の第一歩目は特異クラスである
- 特異メソッドは特異クラスに属する
の3点を抑えれば良いかなと思います。
さて、これを踏まえて、最終章「クラスメソッド」に行きます。
クラスメソッド
クラスメソッドは、クラスから参照できるメソッドであり、クラスからnewしたオブジェクトからは参照できません。
class C def c_instance_method; end # インスタンスメソッド def self.c_class_method; end # クラスメソッド end obj = C.new obj.c_instance_method # => ① nil (メソッドが存在) obj.c_class_method # => ② NoMethodError C.c_instance_method # => ③ NoMethodError C.c_class_method # => ④ nil (メソッドが存在)
ここで、「rubyではクラスもオブジェクトである」ということに着目すると、
クラスメソッドは、そのクラスの特異メソッドである
ということが言えます。
つまり、クラスメソッドはどこにいるかというと、下図のように「クラスの特異クラス」にいるのです。
これを頭に入れた上で、上記の4パターンの探索を見てみましょう。
① obj.c_instance_method # => nil(メソッドが存在)
- レシーバは obj
- objの特異クラスである #obj を探索の第一歩とする
- 見つからないので superclass を探索
- 見つかったのでメソッドを実行
② obj.c_class_method # => NoMethodError
- レシーバは obj
- objの特異クラスである #obj を探索の第一歩とする
- 見つからないので superclass を探索
- BasicObject まで探しても見つからないので
NoMethodError
③ C.c_instance_method # => NoMethodError
この場合、一気に複雑になります。図と箇条書きの説明をひとつずつ照らしあわせてください。
- レシーバは C
- Cの特異メソッドである #C を探索の第一歩とする
- 見つからないので superclass を探索
- #C の superclass は、「C の superclass である Object」の特異クラス #Object である
- その要領で #BasicObject まで探索する
- 特異クラス #BasicObject の superclass は Classクラス
- 引き続き
Class → Module → Object → BasicObject
と探索する- BasicObject まで探しても見つからないので
NoMethodError
ここで、気をつける点があります。図を見ながら追ってみてください。
- 一般的なクラス(
Classクラス
のオブジェクト)の「特異クラスのsuperclass」は そのクラスのsuperclassの特異クラス であるC.singleton_class.superclass # => #<Class:Object>
C.superclass.singleton_class # => #<Class:Object>
- 同じ!
- 一般的なオブジェクト(任意のクラスからnewされたもの)の「特異クラスのsuperclass」は そのオブジェクトの参照するクラス である
obj.singleton_class.superclass # => C
obj.class # => C
- 同じ!
④ C.c_class_method # => nil (メソッドが存在)
これはもう簡単ですね。
- レシーバは C
- Cの特異メソッドである #C を探索の第一歩とする
- 見つかったのでメソッドを実行
いかがでしたでしょうか。
③ が特に複雑です。本当にそうなっているのか?と思ったので、pry で確かめてみました。
Cから特異クラスに渡り、そこからひたすらsuperclassを追っていきます。
[1] pry(main)> class C;end => nil [2] pry(main)> C.singleton_class => #<Class:C> [3] pry(main)> C.singleton_class.superclass => #<Class:Object> [4] pry(main)> C.singleton_class.superclass.superclass => #<Class:BasicObject> [5] pry(main)> C.singleton_class.superclass.superclass.superclass => Class [6] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass => Module [7] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass => Object [8] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass.superclass => BasicObject [9] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass.superclass.superclass => nil
ちゃんと、メソッド探索の順になっていますね。
まとめ
いかがでしたでしょうか。メソッド探索のイメージが湧きましたでしょうか。
今回のおさらいは以下の項目です。ここまで読んでくださった方なら、すんなりイメージできると思います。
- オブジェクトは「インスタンス変数」と「クラスへの参照」を持つ
- クラスは「インスタンスメソッド」を持つ
- 全てのオブジェクトは必ず特異クラスを持つ
- メソッド探索の第一歩目は特異クラスである
- メソッドが見つからなければsuperclassを探索する
- 一般的なクラス(
Classクラス
のオブジェクト)の「特異クラスのsuperclass」は そのクラスのsuperclassの特異クラス である- 一般的なオブジェクト(任意のクラスからnewされたもの)の「特異クラスのsuperclass」は そのオブジェクトの参照するクラス である
- BasicObject まで探索してもメソッドが見つからなければ
NoMethodError
「メタプログラミング Ruby」では、このようなメソッド探索を、
「レシーバから右へ一歩、そこからは上へ」
と表現しています。
なるほど、確かにそのような動きをしますね。
- オブジェクトがレシーバでも「レシーバから右へ一歩、そこからは上へ」
- クラスがレシーバでも「レシーバから右へ一歩、そこからは上へ」
となっていることに気づくと思います。
これで、「あれ?メソッド書いたのになんでNoMethodError
なんだろう」ということはなくなりそうです。
ぜひ、メソッド迷子の手助けになればmm