26
21

[Ruby]ポケモンで理解する特異メソッド・特異クラスの旅

Last updated at Posted at 2024-03-04

Ruby独特の概念に特異メソッド・特異クラスがあります。
この特異メソッド・特異クラスはRuby世界でのオブジェクトが独自の振る舞いをするために欠かすことができない概念です。

そこで今回は、ポケモンを例にしながら特異メソッド・特異クラスについて説明していきます!

この記事の目的

特異メソッド・特異クラスについて理解する

サンプルコード

今回は以下のサンプルコードで説明していきます。

index.rb
class Pokemon
  attr_reader :name,
              :attack_point

  def initialize(name, attack_point)
    @name = name
    @attack_point = attack_point
  end

  def attack
    puts "#{@name}の通常攻撃!#{attack_point}のダメージを与えた!"
  end
end

class Pikachu < Pokemon
  def kaminari_attack
    puts "#{@name}のカミナリ!#{attack_point * 3}のダメージを与えた!"
  end
end

satoshi_no_pikachu = Pikachu.new('ピカ様', 50)

satoshi_no_pikachu.attack
satoshi_no_pikachu.kaminari_attack
# => ピカ様の通常攻撃!50のダメージを与えた!
# => ピカ様のカミナリ!150のダメージを与えた!

サンプルコードでは以下が実装されています。

  1. Pokemonクラスとそのインスタンスメソッドであるattackメソッド
  2. Pokemonクラスを継承したPikachuクラスとそのインスタンスメソッドであるkaminari_attackメソッド
  3. Pikachuクラスのオブジェクトであるピカ様

ピカ様はPikachuクラスのオブジェクトなのでkaminari_attackattackの両方を使用できます。
このサンプルコードを使って、ピカ様に特異メソッドを追加していきましょう!

特異メソッドとはなんだ?

Rubyでは特定のオブジェクトにメソッドを追加することができます。
例えば、ピカ様だけに「なみのり」を覚えさせたいとします。
このとき、以下のようにオブジェクト.メソッド名でメソッドを定義します。

def satoshi_no_pikachu.naminori
  puts "#{@name}のなみのり!#{attack_point * 4}のダメージを与えた!"
end

satoshi_no_pikachu.naminori
# => ピカ様のなみのり!200のダメージを与えた!

もしくはオブジェクト.define_singleton_methodで定義することもできます。この場合、引数がメソッド名、渡したブロック内がメソッドの処理になります。

satoshi_no_pikachu.define_singleton_method :naminori do
  puts "#{@name}のなみのり!#{attack_point * 5}のすごいダメージを与えた!ついでに相手を麻痺状態にした!"
end

naminoriメソッドはピカ様オブジェクトだけに追加されたメソッドです。
このように単一のオブジェクトに特化したメソッドを特異メソッドと呼びます。

特異メソッドはそのオブジェクト固有のメソッドのため、他のオブジェクトから呼び出すことはできません。
そのため、Pikachuクラスの別のオブジェクトがnaminoriメソッドを使用するとエラーになります。

index.rb
normal_pikachu = Pikachu.new('普通のピカチュウ', 30)
normal_pikachu.naminori
# =>  undefined method `naminori' for #<Pikachu:0x00007fb3d6088658> (NoMethodError)

特異メソッドが定義されているかどうかはsingleton_methodsで確認できます。

puts "普通のピカチュウの特異メソッド:#{normal_pikachu.singleton_methods}"
puts "ピカ様の特異メソッド:#{satoshi_no_pikachu.singleton_methods}"
# => 普通のピカチュウの特異メソッド:[]
# => ピカ様の特異メソッド:[:naminori]

特異メソッドを使用することで、同じPikachuクラスから生成されたピカ様と普通のピカチュウに、別の振る舞いをさせることができるようになります。

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

ここまではクラスから生成したオブジェクトに対して特異メソッドを定義しました。
では、クラス自身にも特異メソッドは定義できるのでしょうか?
実は、クラス自身に特異メソッドを定義することもできます。一般的にはクラスメソッドと呼ばれます。

なぜクラスメソッド=特異メソッドなのかについて、RUby世界のオブジェクトについて簡単に紹介しながら説明していきます!

Ruby世界のオブジェクトとは何か?

クラスメソッドがクラスの特異メソッドであることを理解するためには、まずRuby世界のクラスもオブジェクトであることを理解する必要があります。

ではそもそも、オブジェクトとは何でしょうか?
Ruby世界のオブジェクトは

  1. インスタンス変数を持つことができる
  2. クラスから生成される
  3. メソッドを使用できる

の3つの特徴があります。例えば、前述のピカ様で①~③を確認すると、以下のようになります。
ピカ様はPikachuクラスから生成されたオブジェクトですね。
@name@attack_pointという二つのインスタンス変数を持っており、色々なメソッドも使うことができます。

puts satoshi_no_pikachu.instance_variables
puts satoshi_no_pikachu.class
puts satoshi_no_pikachu.methods
# => @name
# => @attack_point
# => Pikachu
# => naminori kaminari_attack name attack_point attack instance_variable_defined? ...etc

ただ、実はオブジェクト自身にメソッドは定義されていません。
例えば、ピカ様が使用するkaminari_attackPikachuクラスのインスタンスメソッドですし、attackメソッドはPokemonクラスのインスタンスメソッドです。
オブジェクト自身はメソッドを持っておらず、オブジェクトは自分が生成されたクラスやそのスーパークラスのメソッドを参照して呼び出しているわけですね。
図にすると以下のような感じです。

段落テキスト.png

そのため、先ほど定義したnaminoriメソッドも実はピカ様オブジェクト自身に定義しているのではなく、別のところで定義されています。その別のところで定義したnaminoriメソッドを参照することでピカ様は波乗りを使うことができます。

Ruby世界のクラスとは何か?

Ruby世界では「クラスはオブジェクト」です。そのためオブジェクトに当てはまる特徴はクラスにも当てはまります。例えば、PikachuクラスはClassクラスのオブジェクトであることがわかります。
Ruby世界のすべてのクラス(Pikachuクラス、Pokemonクラス、Stringクラスなど)はClassクラスから生成されたオブジェクトです。

puts Pikachu.instance_variables
puts Pikachu.class
puts Pikachu.methods
# => *Pikachuオブジェクトはインスタンス変数を持っていないため空で出力される
# => Class
# => allocate superclass new <=> ...etc

クラスに特異メソッドを定義する

Ruby世界のすべてのクラスはオブジェクトです。
クラスはオブジェクトのため、先ほどピカ様オブジェクトにnaminoriメソッドを追加したようにPikachuクラスに特異メソッドを追加することができます。
追加方法は3通りです。

  1. オブジェクト名+メソッド名(Pikachu.racial_value_hp
  2. クラス内でself参照+メソッド名(self.racial_value_attack
  3. class << self内でメソッド名のみで定義(racial_value_defense
index.rb
class Pikachu < Pokemon
  def Pikachu.racial_value_hp
    puts 'ピカチュウのHP種族値は35'
  end

  def self.racial_value_attack
    puts 'ピカチュウのこうげき種族値は55'
  end

  class << self
    def racial_value_defense
      puts 'ピカチュウのぼうぎょ種族値は40'
    end
  end
  
  def kaminari_attack
    puts "#{@name}のカミナリ!#{attack_point * 3}のダメージを与えた!"
  end
end

Pikachu.racial_value_hp
Pikachu.racial_value_attack
Pikachu.racial_value_defense
# => ピカチュウのHP種族値は35
# => ピカチュウのこうげき種族値は55
# => ピカチュウのぼうぎょ種族値は40

上記で追加したメソッドはPikachuクラス自身が呼び出せるメソッドであるため、一般的にクラスメソッドと呼ばれます。

クラスメソッドは、特定のオブジェクト(Pikachuクラス)にのみ定義されている特別なメソッドのため特異メソッドになります。
Classクラスから生成されているすべてのクラスのうちPikachuクラスだけがracial_value_hpを使用できるので、特異メソッドになるということですね。

特異クラスとはなんだ?

では、これらの特異メソッドはどこに定義されているのでしょうか?

例えば、ピカ様にnaminoriメソッドを定義しましたがこのメソッドはどこにいるのでしょうか?
ピカ様オブジェクト自身にはいません。
なぜなら、オブジェクト自身はメソッドを持たないからです。

じゃあPikachuクラスでしょうか?
でもPikachuクラスにいると仮定すると、普通のピカチュウオブジェクトもnaminoriメソッドを使えてしまいます。
それはまずい。。。

実は特異メソッドは特異クラスという場所にいます。特異クラスは特異メソッドを定義するための場所なのです。

特異クラスはsingleton_classメソッドで確認できます。

puts satoshi_no_pikachu.singleton_class
puts Pikachu.singleton_class
# => #<Class:#<Pikachu:0x00007fd8ce05d210>>
# => #<Class:Pikachu>

この特異クラスには以下の特徴があります。

  1. インスタンスを一つしか持てない(そのため、シングルトンクラスとも呼ばれる)
  2. 特異クラスにはオブジェクトの特異メソッドが定義されている

特異クラスは、そのオブジェクト固有の特異メソッドを定義する場所のため、必ず一つのオブジェクトとだけ紐付きます。

複数のオブジェクトと紐づいてしまうと、特異メソッドが複数のオブジェクトで使えてしまうからです。

では、特異クラスに特異メソッドが定義されていることを確認してみましょう。
確認するためには、特異クラスにinstance_methodsメソッドを使います。

# ピカ様の特異クラスに、naminoriメソッドが定義されていることを確認
puts satoshi_no_pikachu.singleton_class.instance_methods(false)
# => naminori

# Pikachuクラスの特異クラスに、Pikachuクラスのクラスメソッドが定義されていることを確認
puts Pikachu.singleton_class.instance_methods(false)
# => racial_value_hp racial_value_attack racial_value_defense

やっと見つけましたね!
今まで定義してきた特異メソッドたちは、特異クラスのインスタンスメソッドとして定義されていました!

ピカ様はnaminoriを使用するときに、自身の特異クラスにインスタンスメソッドとして定義されているnaminoriメソッドを参照します。

これは、kaminariを使用するときに、自身を生成したクラスであるPikachuクラスのインスタンスメソッドを参照するのと同じですね。

同じようにPikachuクラスがracial_value_hpを使用するときは、自身の特異クラスにインスタンスメソッドとして定義されているインスタンスメソッドを参照して実行するわけですね。
そして、クラスに定義されたインスタンスメソッド(クラスから生成されたオブジェクトが使用できる)と区別する意味をこめて、クラス自身が実行できる特異メソッドをクラスメソッドと呼んでいるわけです。

特異メソッドの探索順序

最後に特異メソッドの探索順序について簡単に紹介します!

メソッドの探索順序をわかりやすく理解するために、以下のような出来事がポケモン世界で起きたとします。

ポケモン世界で革新が起こり、全てのポケモンが波乗りを使えるようになったとします。
その中でピカチュウたちは、波乗りに電流を乗せ、相手を必ず麻痺させることができるようになりました。
特にピカ様は一流の波乗り使いで、麻痺させるとともに威力を通常の5倍にすることができました。

上記のようなポケモン世界は以下のコードで実現できます。

index.rb
class Pokemon
  attr_reader :name,
              :attack_point

  def initialize(name, attack_point)
    @name = name
    @attack_point = attack_point
  end

  def naminori
    puts "#{@name}のなみのり!#{attack_point}のダメージを与えた!"
  end
end

class Pikachu < Pokemon
  def naminori
    puts "#{@name}のなみのり!#{attack_point}のダメージを与えた!ついでに相手を麻痺状態にした!"
  end
end

satoshi_no_pikachu = Pikachu.new('ピカ様', 50)
normal_pikachu = Pikachu.new('普通のピカチュウ', 30)

def satoshi_no_pikachu.naminori
  puts "#{@name}のなみのり!#{attack_point * 5}のすんごいダメージを与えた!ついでに相手を麻痺状態にした!"
end

satoshi_no_pikachu.naminori
normal_pikachu.naminori
# => ピカ様のなみのり!250のすごいダメージを与えた!ついでに相手を麻痺状態にした!
# => 普通のピカチュウのなみのり!30のダメージを与えた!ついでに相手を麻痺状態にした!

普通のピカチュウはPikachuクラスに実装されているnaminoriメソッドを実行しています
一方で、ピカ様は自身の特異クラスに定義されているnaminoriメソッドを実行しています。

これは以下のような流れで使うメソッドを探しているからです。

まず特異メソッドを定義していない普通のピカチュウオブジェクトは、自身を生成したPikachuクラスから順番にクラスの継承をさかのぼってnaminoriメソッドを探します。
クラスの継承はクラス.ancestorsで確認できます。

puts Pikachu.ancestors
# => Pikachu Pokemon Object Kernel BasicObject

Pikachuクラス→Pokemonクラス→Objectクラス→Kernelモジュール1BasicObjectクラスの順番ですね。

一方でピカ様のように特異メソッドを定義すると、メソッドの探索順序は特異クラスが最優先になります。
ピカ様の特異クラスPikachuクラス→Pokemonクラス→Objectクラス→Kernelモジュール→BasicObjectクラスの順番ですね。

そのため普通のピカチュウとピカ様はそれぞれ別のnaminoriメソッドを使用することができます。

また、もし仮に特異クラスとPikachuクラスのどちらにもnaminoriメソッドがない場合、Pokemonクラスのnaminoriメソッドが実行されます。

まとめ

  • 特異メソッドはオブジェクト固有のメソッドを定義する
  • 特異クラスは特異メソッドを定義している場所
  • メソッドの探索順序はまず特異クラスが最優先。その後クラスの継承をさかのぼっていく

参考書籍

メタプログラミングRuby第2版

  1. KernelモジュールはObjectクラスに含まれているモジュールのため、厳密に言えばクラスの継承ではありません。

26
21
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
26
21