i Cubed Systems Engineering blog

株式会社アイキューブドシステムズの製品開発メンバーが、日頃のCLOMO開発の様子などを紹介します。

【Ruby】図解・継承チェーンとメソッド探索経路で迷わなくなるために

執筆: Misaki-i3

まえがき

メソッド探索経路を理解するためには、特異クラス・特異メソッド・クラスメソッドという、名前からして特殊そうな概念の把握が必要らしい。
それはそれは難解で恐ろしかろうと私は信じ、Ruby Silverをやりすごせる程度の理解に甘んじて、詳細な仕組みの整理を後回しにしてきた。
Ruby Goldの資格取得のためにいよいよ逃げられなくなり、震える足を踏み出して、何やら様子がおかしいと気付いた。

『特異クラス』は、『クラス』とほとんど変わらなかった。大きな違いは「何をインスタンスとみなしているか」だったのだ。
『特異メソッド・クラスメソッド』に至っては『インスタンスメソッド』そのものだった。

対象レベル

この記事は、上の私の気付きを見て「当たり前のことだ」と感じられる方にとっては有意義でない。また、入門書を開いたばかりの初心者向きでもない。
Rubyの入門書を一周した初心者には十分理解できるものと思う。
探り探り実装をこなしている中級者や、Ruby Goldの合格を目指している学習者にも役立てていただけると期待している。

注意

クラスと継承の話をするには、以下のような概念を区別しなければならない。

  • Class
  • Classのインスタンス(=クラス)
  • クラスのインスタンス(=Classのインスタンスのインスタンス)

私の説明能力では、どうしても「クラス(=Classのインスタンス)のインスタンスの特異クラス」といった混沌が量産されてしまう。
よって、この場に限り、『Classのインスタンス(=クラス)』のことを『クラスオブジェクト』と記述したい。
おそらく、これは公式な用語ではない。

また、記事の内容はRubyに限定した話であり、オブジェクト指向プログラミングの実現方法は言語によって異なることをご留意いただきたい。

準備運動 クラスオブジェクト

クラスはオブジェクト

少なくともRubyにおいては、クラスオブジェクトはただのオブジェクトだ。
文字列がStringクラスのインスタンスオブジェクトであるように、クラスオブジェクトはClassクラスのインスタンスオブジェクトである。

知ってのとおり、classキーワードでクラス定義を実行すると、クラス名の定数でクラスオブジェクトを呼び出せるようになる。
これは、classキーワードがクラスオブジェクトを作成した際、指定された識別子と同名の定数にクラスオブジェクトの参照を代入するためだ。
classキーワードをClass.newに置き換えると簡単に理解できる。

class Sample1; end  # => nil
Sample1             # => Sample1

Sample2 = Class.new do; end # => Sample2
Sample2                     # => Sample2

定数は単なる入れ物だ。クラスオブジェクトの参照さえ入っていれば、samやらhogeやらの変数からでも問題なく呼び出せる。

クラス名は定数名にすぎない?

「classキーワードに与えた識別子が定数名にすぎないのなら、なぜクラスオブジェクトを参照した際、クラス名(Sample1Sample2)を返せたのか?」

その答えは次のとおりだ。

  • 定数に代入されるまでは、クラスオブジェクトは無名である
  • はじめて定数に代入されたとき、クラスオブジェクトはその定数名をクラス名として記憶する

次のコードで、クラスオブジェクトが自分の名前を記憶するタイミングを検証した。

sample3 = Class.new # => #<Class:0x000000010743faa0>
sample3             # => #<Class:0x000000010743faa0>

Sample3 = sample3   # => Sample3
sample3             # => Sample3

Sample3 == sample3  # => true

(ちなみに) クラスオブジェクトを定数に代入→remove_constで定数を削除→同名の定数に別のクラスオブジェクトを代入→remove_constで……
という処理を続けると、複数のクラスオブジェクトが同じ名前を名乗るという状態が実現できた。

Rubyの継承チェーンの全体像

クラスオブジェクトを中心とした継承関係を(できるだけ見やすく)図にまとめた。
白い四角は全てクラスオブジェクトだと考えてほしい。ClassもModuleも、特異クラス(#付きのもの)も、みなクラスオブジェクトだ。

継承チェーンの全体像

注釈のとおり、すべてを暗記する必要はない。

まずは2点だけ押さえておこう。

  1. 基本は『クラスオブジェクトたち > Object > BaseObject』
  2. クラスメソッドの利用時には、特異クラスやClassが継承チェーンに絡んでくる

モジュールについてはあとで説明するので、今は考えなくてよい。
(気になる人のために結論だけ先に言っておくと、クラスに読み込まれたときだけメソッド探索経路にモジュールが追加される)

特異クラス・特異メソッド・クラスメソッドとは結局……

『特異メソッド』と『クラスメソッド』という言葉が、「インスタンスメソッドではない、特別なものがあるらしい」という思い込みの源である

クラスと特異クラス

まずは『クラス』と『特異クラス』がどれだけ違うのかを確かめたい。

結論はすでにまえがきに書いてしまった。

『特異クラス』は、『クラス』とほとんど変わらなかった。大きな違いは「何をインスタンスとみなしているか」だ。

具体的な違いを次の表に示す

クラス 特異クラス
インスタンスとみなすもの 自分がnewしたオブジェクト 特定のオブジェクト
インスタンスとの関係
(呼び出し方)
インスタンス.class インスタンス.singleton_class

(ちなみに: instance_of?メソッドは、クラス.newで作られたインスタンスとクラスの関係判定にしか使えない)

特異メソッドとクラスメソッド

まえがきに書いたもの繰り返す。

『特異メソッド・クラスメソッド』に至っては『インスタンスメソッド』そのものだった。

  • 特異メソッドは、オブジェクトの特異クラスのインスタンスメソッドの別名
  • クラスメソッドは、クラスオブジェクトの特異クラスのインスタンスメソッドの別名

オブジェクトのメソッドを呼び出す命令を与えると、
特異クラスのインスタンスメソッド(=特異メソッド)から探索を開始する。
クラスオブジェクトのメソッドを呼び出す命令を与えると、
特異クラスのインスタンスメソッド(=特異メソッド =クラスメソッド)から探索を開始する。

ほぼ同じだな、と感じていただけただろうか。

特異クラスへのインスタンスメソッド定義

これまでの理解を前提に、特異メソッドを定義するコードを見てみよう。
1) オブジェクトの特異クラスをオープンし、インスタンスメソッドを定義する

class << A
  def method_open; end
end

2) defキーワードで、オブジェクトを指定して定義する

def A.method_def; end

3) オブジェクトにdefine_singleton_methodで定義する

A.define_singleton_method(:method_meta) {}

これらをclassキーワードによる定義内で行うと、見慣れた「クラスメソッド定義」となる。

class A
  self # A

  class << self
    def method_open; end
  end

  def self.method_def; end
end

class << オブジェクトdef オブジェクト.は特異クラスにアクセスするための大事な文法だ。「クラスメソッドを定義するやつ」という不十分な理解になっている場合は覚え直しておきたい。

メソッド探索経路

ここからは教科書どおりの説明を繰り返すだけで十分だろうと思う。

  1. メソッド探索の開始地点は特異クラス
    (※特異クラスにprependされたモジュールがある場合を除く。モジュールについては後述)
  2. 特異クラスにメソッドが見つからなければ、探索経路を辿る
  3. 最後まで見つからなければmethod_missingメソッドを呼び出す

クラスオブジェクトでないオブジェクトの場合

(オブジェクトaのことをインスタンスとみなしているのは、特異クラスaとクラスA)

クラスオブジェクトの場合

(オブジェクトBのことをインスタンスとみなしているのは、特異クラスBとClass)

覚えておくべき経路

次の図に示す範囲は最低限覚えておきたい。

探索経路にモジュールを追加する

モジュールは、インスタンスから呼び出されるためのインスタンスメソッドを定義しておくものだ。つまり、クラスオブジェクトと基本は同じである。

クラスと特異クラスの章で使った表に、モジュールの列を追加してみるとこうなる。

クラス 特異クラス モジュール
インスタンスとみなすもの 自分がnewしたオブジェクト 特定のオブジェクト 自分を読み込んだクラス
にとってのインスタンス
インスタンスとの関係
(呼び出し方)
インスタンス.class インスタンス.singleton_class -

モジュールはクラスオブジェクトに読み込ませて使うものだ。もちろん、特異クラスにも読み込ませることは可能だ。
クラスオブジェクトに読み込まれたモジュールは、クラスオブジェクトのインスタンスのメソッド探索経路に追加される。

追加される位置は、読み込みに使うメソッドによって異なる。

メソッド
include クラスの後に追加する
prepend クラスの前に追加する
extend クラスの特異クラスの後に追加する

extendメソッドを使うと、モジュールのメソッドをクラスメソッドとして追加できる。
これは特異クラスでincludeメソッドを使って読み込むのと同じ動作だ。
「特異クラスをオープンする記述が省略できるよ!」というだけのものなので、怖がらないでほしい。

includeやprependを2度以上記述したときの順番は、「新しい方が手前だ」と暗記するとよい。
(※ include M1, M2のように1度に複数渡した場合は、第一引数側が手前になる)

モジュールが探索経路に追加された状態を図にした。

まとめ

『特異クラス』は、『クラス』とほとんど変わらなかった。大きな違いは「何をインスタンスとみなしているか」だったのだ。
『特異メソッド・クラスメソッド』に至っては『インスタンスメソッド』そのものだった。

まえがきの繰り返しだ。
私が勝手に敵を大きく見積もっていたために遠回りをしてしまっただけとも言える。
しかし、これから学習や試験対策を行う人々が自分と同じ遠回りをするのはもったいないので、私なりに継承チェーンと探索経路の整理と図解を行った。

あとがき

「さき言うといてくれ」と呟きながら学習しています。
複数の参考書を行き来しながら、Webページをブラウザのタブに詰め込みながら、ようやく結び目が解ける情報にたどり着いて視界が開けたとき、「そうならそうと、さき言うてといてくれ」と怒ったふりをしています。

参考書やWebページにある解説にも、筆者らの経験した「さき言うといてくれ」の気持ちが込められているはずなのです。
「あの内容を自分ならこんな説明で――」「あの記事に自分ならこんな注釈を――」
私が1冊の入門書から受け取りきれなかった穴を少しずつ埋めていけたのは、様々な人が「自分なら」をアウトプットしてくれたおかげであることは疑う余地もありません。

弊社では開発部をはじめ、各部門の有志による勉強会が頻繁に開催されています。
発表資料は社員の経験と学習の成果! ときには発表者の「さき言うといてくれ」を感じます。

私も誰かの役に立つことを期待して、アウトプットをこそこそ積ませてもらおうと思います。

また、弊社では採用活動を実施しています。
アウトプットの盛んな社内で、ともに技術の追求を楽しんでいける仲間をお待ちしております。

新卒採用:https://www.i3-systems.com/new-graduates/
キャリア採用:https://www.i3-systems.com/careers/