Effective Rubyの第2章 「クラス、オブジェクト、モジュール」の項目6「Rubyが継承階層をどのように組み立てるかを頭に入れよう」を読んでいて、Rubyの階層構造や特異メソッド、特異クラスについて理解があやふやなところがあったので調査しました。
自分自身、どこまで理解できているのかが曖昧だったので、一歩一歩丁寧に説明していきたいと思います。
対象読者
特異メソッド、特異クラス、クラスメソッドについて、それぞれの定義自体は知っていて、普段問題なく使えているけれど、以下のような問いかけに対して詳しく説明できないなという方。
- 特異メソッドってどんなメソッド?
- クラスメソッドは特異メソッドの一種とはどういうことか
- クラスメソッドはどのクラスに定義されているか
- 特異クラスの親クラスは?
- Object#extend で特異メソッドの定義をしたときの継承階層は?
特異メソッドってどんなメソッド?
まずは特異メソッドの理解を深めるために、以下のようなシンプルなクラスをベースにいじってみます。
class Person
def first_name
'first'
end
def last_name
'last'
end
end
Personクラスから2つのインスタンスを作ってみます。
irb(main):002:0> person = Person.new
irb(main):003:0> another_person = Person.new
そしてperson
の方に特異メソッドの定義をします。
person = Person.new
def person.full_name
'full_name'
end
personに対してはインスタンスメソッドと同じように特異メソッドを呼び出せますが、Personクラスのインスタンスメソッドには定義されていないため、another_personでは呼び出せません。
irb(main):005:0> person.full_name
=> "full_name"
irb(main):006:0> person.class.instance_methods(false)
=> [:first_name, :last_name]
irb(main):007:0> another_person.full_name
NoMethodError (undefined method `full_name' for #<Person:0x00007fd6ca87b560>)
特異メソッドはObject#singleton_methods
で確認できます。
irb(main):009:0> person.singleton_methods
=> [:full_name]
irb(main):010:0> another_person.singleton_methods
=> []
このとき、特異メソッドはperson
というインスタンスに定義されているのではなく、実は特異クラス(シングルトンクラス)に定義されています。
特異クラスとはEffective Rubyには以下のように書かれています。
特異クラスは、継承階層に含まれている名前のない不可視のクラス
また、
特異クラスは、たった1つのオブジェクトのためだけのために働いている
Object#singleton_class
でそのオブジェクトの特異クラスを取得することができます。Object#singleton_class
を呼んだ時点で特異クラスがまだない場合は新しく生成されるようです。
また、特異クラスはもとは同じクラスから生成されたインスタンスであっても、それぞれの特異クラスのobject_id
は異なるという特徴もあります。
irb(main):011:0> person.singleton_class
=> #<Class:#<Person:0x00007fd6ca859d20>>
irb(main):012:0> person.singleton_class.object_id
=> 70280253006480
irb(main):013:0> another_person.singleton_class
=> #<Class:#<Person:0x00007fd6ca87b560>>
irb(main):014:0> another_person.singleton_class.object_id
=> 70280244569720
irb(main):015:0> person.singleton_class.equal?(another_person.singleton_class)
=> false
その他にも特異クラスの特徴として、インスタンスを生成できなかったり、class
メソッドを呼んでも無視されてしまうなどといったものがあります。
irb(main):016:0> person.singleton_class.new
TypeError (can't create instance of singleton class)
irb(main):017:0> person.class
=> Person
先程の特異メソッドはこの特異クラスのインスタンスメソッドに定義されています。
irb(main):018:0> person.singleton_class.instance_methods(false)
=> [:full_name]
irb(main):019:0> another_person.singleton_class.instance_methods(false)
=> []
また、この特異クラスのsuperclass
はPersonとなっており、継承階層を確認できるModule#ancestors
で調べてみると、Personの前に特異クラスが入っていることが確認できます。
irb(main):019:0> person.singleton_class.superclass
=> Person
irb(main):020:0> another_person.class.ancestors
=> [Person, Object, Kernel, BasicObject]
irb(main):021:0> person.singleton_class.ancestors
=> [#<Class:#<Person:0x00007fd6ca859d20>>, Person, Object, Kernel, BasicObject]
クラスメソッドは特異メソッドの一種とはどういうことか
みなさんも一度はクラスメソッドを見たことがあると思います。
以下のように書くとクラスメソッドを定義できます。
class Person
class << self
def where_am_i
'singleton class'
end
end
end
すると、Personをレシーバとしてメソッドを呼び出せるようになります。
irb(main):002:0> Person.where_am_i
=> "singleton class"
また、クラスメソッドはクラス定義の外に出して定義することも可能です。
class Person; end
class << Person
def where_am_i
'singleton class'
end
end
別の記法として以下のように定義することもできます。
class Person
def self.where_am_i
'singleton class'
end
end
またこの記法でも後から定義することが可能です。
class Person; end
def Person.where_am_i
'singleton class'
end
ここまで来ると、先程の特異メソッドの説明で使った記法と似ているので、なんとなく同じものなんだなと感じてきます。
クラスメソッドはどのクラスに定義されているか
クラスメソッドは特異メソッドなので、クラスメソッドが定義されているのは先程と同じで特異クラスのインスタンスメソッドとなります。
PersonのクラスメソッドはPersonの特異クラスのインスタンスメソッドになります。
ここまで来ると、「クラスメソッドなのにインスタンスメソッド?」など、複雑になってくるので挙動を確認してみます。
irb(main):010:0> Person.singleton_methods
=> [:where_am_i]
irb(main):012:0> Person.singleton_class
=> #<Class:Person>
irb(main):014:0> Person.singleton_class.instance_methods(false)
=> [:where_am_i]
確かに、Personの特異メソッドは、特異クラスのインスタンスメソッドに定義されていそうです。
先程の「クラスメソッドなのにインスタンスメソッド?」と難しそうな理由としては、PersonのようなクラスはClass
クラスのインスタンスであるからかなと思います。
irb(main):005:0> Person.class
=> Class
irb(main):006:0> Person.singleton_class.class
=> Class
最初の例で以下のように試しましたが、
person = Person.new
def person.full_name
'full_name'
end
これと似た感じに書くと、以下のように書くことができます。
上の例とほぼ同じですね。
Person = Class.new
def Person.where_am_i
'singleton class'
end
ちなみに上記の例で示した、class << obj
を使ったクラスメソッドの定義は特異クラスを定義する構文のようです。
特異クラスの親クラスは?
もう少し特異クラスについて調べてみます。
特異メソッドを持ったPersonクラスを継承したCustomerクラスを定義してみます。Class#superclass
を使うと確認することができます。
irb(main):007:0> class Customer < Person; end
irb(main):008:0> Customer.superclass
=> Person
irb(main):009:0> Customer.singleton_class
=> #<Class:Customer>
irb(main):010:0> Customer.singleton_class.superclass
=> #<Class:Person>
irb(main):011:0> Person.singleton_class
=> #<Class:Person>
上記のように、Customerの特異クラスの親クラスは、Personの特異クラスになっています。
そのため、以下のようにCustomerに対してもPersonに定義されている特異メソッドを呼び出すことができるようになっています。
irb(main):012:0> Customer.where_am_i
=> "singleton class"
irb(main):013:0> Customer.singleton_methods
=> [:where_am_i]
Object#extend で特異メソッドの定義をしたときの継承階層は?
Object#extend
でも特異メソッドを定義できるので、この挙動についても調べてみます。
はじめに以下のようにThingsWithNames
モジュールを定義します。
module ThingsWithNames
def things_with_name
'things with names'
end
end
class Person; end
Module#include
extend
の前に比較としてinclude
の挙動を確認します。
include
をすると継承階層の間にモジュールが挿入され、インスタンスメソッドを追加することができます。
irb(main):002:0> ThingsWithNames.instance_methods(false)
=> [:things_with_name]
irb(main):003:0> Person.include ThingsWithNames
=> Person
irb(main):004:0> Person.ancestors
=> [Person, ThingsWithNames, Object, Kernel, BasicObject]
irb(main):005:0> Person.instance_methods.include?(:things_with_name)
=> true
irb(main):006:0> Person.new.things_with_name
=> "things with names"
Object#extend
一方extend
は特異メソッドとして追加することができます。
ただしextend
による特異メソッドの定義は、Personの特異クラスに直接特異メソッドが追加されているのではありません。
特異クラスの継承階層の間にモジュールが追加されることで、特異クラスのインスタンスメソッドとして呼び出すことができるようになっています。
irb(main):002:0> Person.extend ThingsWithNames
=> Person
irb(main):003:0> Person.things_with_name
=> "things with names"
irb(main):004:0> Person.singleton_methods(false)
=> []
irb(main):005:0> Person.singleton_methods(true)
=> [:things_with_name]
irb(main):006:0> Person.singleton_class.ancestors
=> [#<Class:Person>, ThingsWithNames, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
irb(main):007:0> Person.singleton_class.instance_methods.include?(:things_with_name)
=> true
ちなみに、extend
する前のsingleton_class
のancestors
はこんな感じになっており、上記の結果と比較するとThingsWithNames
が継承階層の間に追加されていることがわかります。
irb(main):007:0> Person.singleton_class.ancestors
=> [#<Class:Person>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
おわりに
Rubyが勝手にうまく動かしてくれているおかげで、このあたりをあまり意識せずにクラスに対してメソッド呼び出しができていましたが、実際に一歩一歩動かしながら挙動を確認することで理解を深めることができました。
参考