Ruby独特の概念に特異メソッド・特異クラスがあります。
この特異メソッド・特異クラスはRuby世界でのオブジェクトが独自の振る舞いをするために欠かすことができない概念です。
そこで今回は、ポケモンを例にしながら特異メソッド・特異クラスについて説明していきます!
この記事の目的
特異メソッド・特異クラスについて理解する
サンプルコード
今回は以下のサンプルコードで説明していきます。
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のダメージを与えた!
サンプルコードでは以下が実装されています。
-
Pokemon
クラスとそのインスタンスメソッドであるattack
メソッド -
Pokemon
クラスを継承したPikachu
クラスとそのインスタンスメソッドであるkaminari_attack
メソッド -
Pikachu
クラスのオブジェクトであるピカ様
ピカ様はPikachu
クラスのオブジェクトなのでkaminari_attack
とattack
の両方を使用できます。
このサンプルコードを使って、ピカ様に特異メソッドを追加していきましょう!
特異メソッドとはなんだ?
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
メソッドを使用するとエラーになります。
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世界のオブジェクトは
- インスタンス変数を持つことができる
- クラスから生成される
- メソッドを使用できる
の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_attack
はPikachu
クラスのインスタンスメソッドですし、attack
メソッドはPokemon
クラスのインスタンスメソッドです。
オブジェクト自身はメソッドを持っておらず、オブジェクトは自分が生成されたクラスやそのスーパークラスのメソッドを参照して呼び出しているわけですね。
図にすると以下のような感じです。
そのため、先ほど定義した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通りです。
- オブジェクト名+メソッド名(
Pikachu.racial_value_hp
) - クラス内でself参照+メソッド名(
self.racial_value_attack
) -
class << self
内でメソッド名のみで定義(racial_value_defense
)
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>
この特異クラスには以下の特徴があります。
- インスタンスを一つしか持てない(そのため、シングルトンクラスとも呼ばれる)
- 特異クラスにはオブジェクトの特異メソッドが定義されている
特異クラスは、そのオブジェクト固有の特異メソッドを定義する場所のため、必ず一つのオブジェクトとだけ紐付きます。
複数のオブジェクトと紐づいてしまうと、特異メソッドが複数のオブジェクトで使えてしまうからです。
では、特異クラスに特異メソッドが定義されていることを確認してみましょう。
確認するためには、特異クラスに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倍にすることができました。
上記のようなポケモン世界は以下のコードで実現できます。
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
モジュール1→BasicObject
クラスの順番ですね。
一方でピカ様のように特異メソッドを定義すると、メソッドの探索順序は特異クラスが最優先になります。
ピカ様の特異クラス
→Pikachu
クラス→Pokemon
クラス→Object
クラス→Kernel
モジュール→BasicObject
クラスの順番ですね。
そのため普通のピカチュウとピカ様はそれぞれ別のnaminori
メソッドを使用することができます。
また、もし仮に特異クラスとPikachu
クラスのどちらにもnaminori
メソッドがない場合、Pokemon
クラスのnaminori
メソッドが実行されます。
まとめ
- 特異メソッドはオブジェクト固有のメソッドを定義する
- 特異クラスは特異メソッドを定義している場所
- メソッドの探索順序はまず特異クラスが最優先。その後クラスの継承をさかのぼっていく
参考書籍
-
Kernel
モジュールはObject
クラスに含まれているモジュールのため、厳密に言えばクラスの継承ではありません。 ↩