11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

一歩ずつ理解する特異メソッドと特異クラス

Last updated at Posted at 2021-11-04

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_classancestorsはこんな感じになっており、上記の結果と比較するとThingsWithNamesが継承階層の間に追加されていることがわかります。

irb(main):007:0> Person.singleton_class.ancestors
=> [#<Class:Person>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

おわりに

Rubyが勝手にうまく動かしてくれているおかげで、このあたりをあまり意識せずにクラスに対してメソッド呼び出しができていましたが、実際に一歩一歩動かしながら挙動を確認することで理解を深めることができました。

参考

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?