Rubyのオブジェクトモデルを理解する
概要
この記事は、「メタプログラミングRuby 第2版」第2章「オブジェクトモデル」を読み学習した内容を個人学習用にまとめ直したものです。
この記事では、Rubyのメタプログラミングを理解する上で、前提知識となるオブジェクトモデルの基礎知識について説明しています。
元の書籍はRuby 2.xを取り扱っていますが、執筆時点でのRuby 4.0の知識へアップデートしています。
オープンクラス
Rubyではクラスは常にオープンであり、既存のクラスに後からメソッドを追加できる。
# 標準のStringクラスにメソッドを追加する例
class String
def to_alphanumeric
gsub(/[^\w\s]/, '')
end
end
モンキーパッチ
オープンクラスは強力な機能だが、既存のクラスを変更することで予期しない副作用を引き起こす可能性がある。これは「モンキーパッチ」と呼ばれ、既存のメソッドを上書きしてしまうと、そのクラスに依存している他のコードが正常に動作しなくなる危険性がある。使用する際は慎重に検討する必要がある。
オブジェクトモデルの内部
オブジェクトの構成要素
Rubyのオブジェクトは、インスタンス変数とクラスへのリンクで構成されている。
- インスタンス変数
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
obj.instance_variables # => [:@v]
- メソッド
オブジェクトのメソッドはオブジェクトのクラスに存在している(インスタンスメソッド)。
# インスタンスメソッド
String.instance_methods == "abc".methods # => true
# クラスメソッド
String.methods == "abc".methods # => false
🧍 user オブジェクト
┌─────────────────┐
│ データを持つ箱 │
│─────────────────│
│ @name = "Taro" │
│ @age = 20 │
└─────────────────┘
│
│「メソッドはどこ?」
▼
📘 User クラス(設計図)
┌─────────────────┐
│ ふるまいを定義 │
│─────────────────│
│ name │
│ age │
│ greet │
└─────────────────┘
クラスの実態
クラスは Class クラスのオブジェクトである。
"hello".class # => String
String.class # => Class
Java など、他のオブジェクト指向言語と違い、RubyのClassクラスのインスタンスは実行時に柔軟に情報を書き込むことができる(オープンクラス)。
また、「オブジェクトのメソッドは、そのクラスのインスタンスメソッド」であるので、クラスのメソッドは、Classクラスのインスタンスメソッドとなる。
# 引数のfalseは継承したメソッドは無視するという意味
Class.instance_methods(false) # => [:allocate, :new, :superclass, :subclasses, :attached_object]
各メソッドの補足
-
allocate:newと違い、initializeを呼び出さずにインスタンスを生成 -
subclasses: 直接のサブクラスの配列を返す(Ruby 3.1で追加) -
attached_object: シングルトンクラスの対象オブジェクトを返す(Ruby 3.2で追加)
モジュール
Module は Class のスーパークラスとなる。
Class.superclass # => Module
つまり、Classの実態とはオブジェクトの生成やクラスの階層構造を扱うためのインスタンスメソッド(new, allocate, superclass, subclasses, attached_object)を追加したモジュールである。
モジュール自体は、基本的にはメソッドをまとめただけのもの。
定数
大文字で始まる参照は、クラス名やモジュール名を含めて、全てが定数である。
定数はファイルシステムのようなツリー状に配置されており、モジュールやクラスの名前がディレクトリ、通常の定数がファイルのようになっている。
定数のパスは::で区切る。
module M
class C
X = "ある定数"
end
C::X # => "ある定数"
end
M::C::X # => "ある定数"
ネームスペースとしてのモジュール
モジュールは定数の名前空間を提供する。これにより、異なるライブラリやモジュール間で同じ名前の定数やクラスを使用しても衝突を避けることができる。例えば、ActiveRecord::BaseとMyApp::Baseは異なるクラスとして扱われる。
メソッドの探索と継承チェーン
メソッド探索のルール
メソッドを呼び出すと、Rubyはレシーバのクラスに向かって一歩右へ進み、それから継承チェーンが終わるまで探索を続ける。
+-----------------------+
| BasicObject | (3) find foo
+-----------------------+
^
| not found
+-----------------------+
| Object | (2) find foo
+-----------------------+
^
| not found
+-----------------------+ +-----------------------+
| obj | ---> | String | (1) find foo (start)
| (instance) | +-----------------------+
+-----------------------+
モジュールの挿入
モジュールをincludeした場合、そのクラスの継承チェーンの真上にモジュールが挿入される。
class String
include Module
end
+-----------------------+
| BasicObject |
| (4) find foo |
+-----------------------+
^
| not found
+-----------------------+
| Object |
| (3) find foo |
+-----------------------+
^
| not found
+-----------------------+
| Module (included) |
| (2) find foo |
+-----------------------+
^
| not found
+-----------------------+ +-----------------------+
| obj | ---> | String |
| (instance) | | (1) find foo |
+-----------------------+ +-----------------------+
多重include
同じモジュールを複数回includeしても、2回目以降は無視される。
module M1
end
module M2
include M1 # M2の中でM1をinclude
end
class MyClass
include M2 # M2をinclude(M1も自動的に含まれる)
include M1 # M1を再度includeしても無視される
end
MyClass.ancestors # => [MyClass, M2, M1, Object, Kernel, BasicObject]
# M1は一度しか含まれない
モジュールをprependした場合、そのクラスの継承チェーンの真下にモジュールが挿入される。
class String
prepend Module
end
+-----------------------+
| BasicObject |
| (4) find foo |
+-----------------------+
^
| not found
+-----------------------+
| Object |
| (3) find foo |
+-----------------------+
^
| not found
+-----------------------+
| String |
| (2) find foo |
+-----------------------+
^
| not found
+-----------------------+
| Module (prepended) |
| (1) find foo |
+-----------------------+
^
| (lookup starts here)
+-----------------------+ |
| obj | -----------+
| (instance) |
+-----------------------+
selfとは?
- オブジェクトがメソッドを呼び出すとき、そのレシーバとなるオブジェクト自身が
self - モジュールあるいはクラスを定義する時は、そのモジュールが
self - 全てのインスタンス変数は
selfのインスタンス変数 - レシーバを明示しないメソッド呼び出しは
selfのメソッドと見なされる
class MyClass
puts self # => MyClass(クラス定義時)
def my_method
@v = 1 # selfのインスタンス変数
another # self.another と同じ
puts self # レシーバオブジェクト
end
def another; end
end
obj = MyClass.new
obj.my_method # => #<MyClass:0x...>
Refinements
Refinementsによって、モンキーパッチと同じようにメソッドをオーバーライドし、その影響範囲をグローバルでなく、usingで呼び出したファイルやモジュールの定義の終わりまでに限定できる。
module StringExtentions
refine String do
def reverse
"esrever"
end
end
end
module StringStuff
using StringExtentions
"my_string".reverse # => "esrever"
end
"my_string".reverse # => "gnirts_ym"
まとめ
- Rubyのクラスは常にオープンで、後からメソッドを追加できる
- オブジェクトはインスタンス変数とクラスへのリンクで構成される
- クラスも
Classクラスのオブジェクト。ClassはModuleに3つのメソッドを追加したもの - メソッド探索は継承チェーン(ancestors)を辿る
-
includeはクラスの真上、prependは真下にモジュールを挿入 -
selfはメソッドのレシーバオブジェクト自身 -
Refinementsでモンキーパッチの影響範囲を限定できる
参考文献
この記事は以下の情報を参考にして執筆した。