圧倒亭グランパのブログ

30年後の自分にもわかるように書くブログ

【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

当初、自分はオブジェクトとクラスの関係を以下のように考えていました。
(図は本を参考にした独自のものです。クラス図とは関係ありません。)

f:id:at_grandpa:20160201020553p:plain:w300

  • クラスCは雛形
  • 雛形に合わせてobjオブジェクトが生成される

しかし、ruby本を読んでいると、実際はこうでした。

f:id:at_grandpa:20160201020600p:plain:w300

  • オブジェクトは インスタンス変数クラスへの参照 を持つ
  • クラスは インスタンスメソッド を持つ

一番驚いたのは、「オブジェクトの中にはメソッドが無い」ということ。

これはどういうカラクリかというと、objのメソッド呼び出しの時にわかります。

 

メソッド探索

サンプルコードでメソッドを呼び出してみましょう。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.c_instance_method # ここでインスタンスメソッドを呼び出した

この時、何が起きるかというと、rubyは以下のようにメソッドを探索します。

f:id:at_grandpa:20160201022623p:plain:w700

  • 参照しているクラスにメソッドを探索しにいく
  • 見つかったら実行

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 # インスタンスメソッドを呼び出す

以下のようになります。

f:id:at_grandpa:20160201023219p:plain:w700

  • インスタンスメソッドはクラスにあり、同じクラスから生成されたオブジェクトでは共通のメソッドを実行する
  • インスタンス変数はオブジェクトにあり、それらは別々のもの

では、存在しないメソッドを呼び出した場合はどうなるのでしょうか。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.missing_method # 存在しないメソッドを呼び出した

rubyは、メソッドが見つからなければsuperclassをどんどん遡って探索します。

f:id:at_grandpa:20160201030738p:plain:w700

  • メソッドが見つからなければ、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 # インスタンスメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160201030756p:plain:w600

  • はじめに探索するのは、オブジェクトの参照しているクラス(今回は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 # モジュールのメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160202145825p:plain:w600

  • 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のメソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160203002548p:plain:w600

  • 後に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 # モジュールのメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160203092729p:plain:w600

  • 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のメソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160203093017p:plain:w600

  • 後に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 # 特異メソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160204095424p:plain:w600

  • 実は、各オブジェクトは必ず一つの特異クラスを持っている
  • 実は、メソッド探索では、特異クラスを一番最初に探索していた
    • 今までは特異クラスにメソッドが見つからなかったので素通りしていた
  • 特異メソッドは特異クラスに属する
    • 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ではクラスもオブジェクトである」ということに着目すると、

 クラスメソッドは、そのクラスの特異メソッドである

ということが言えます。

つまり、クラスメソッドはどこにいるかというと、下図のように「クラスの特異クラス」にいるのです。

f:id:at_grandpa:20160214082739p:plain:w760

これを頭に入れた上で、上記の4パターンの探索を見てみましょう。

 

① obj.c_instance_method # => nil(メソッドが存在)

f:id:at_grandpa:20160214082908p:plain:w760

  • レシーバは obj
  • objの特異クラスである #obj を探索の第一歩とする
  • 見つからないので superclass を探索
  • 見つかったのでメソッドを実行

 

② obj.c_class_method # => NoMethodError

f:id:at_grandpa:20160214083011p:plain:w760

  • レシーバは obj
  • objの特異クラスである #obj を探索の第一歩とする
  • 見つからないので superclass を探索
  • BasicObject まで探しても見つからないので NoMethodError

 

③ C.c_instance_method # => NoMethodError

この場合、一気に複雑になります。図と箇条書きの説明をひとつずつ照らしあわせてください。

f:id:at_grandpa:20160214080300p:plain:w780

  • レシーバは 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 (メソッドが存在)

これはもう簡単ですね。

f:id:at_grandpa:20160214083548p:plain:w790

  • レシーバは 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