Skip to main content

メタプログラミングRuby 5章 クラス定義

特異メソッド

カレントクラス

Rubyのプログラムは常にカレントオブジェクトselfを持っているが、同時にカレントクラスも持っています。

 class C
  def m1
    p self
    def m2; end
  end
end

class D < C; end

obj = D.new
obj.m1 # => <D:0x00007fc1e2865938>

p C.instance_methods(false) # =>[:m1, :m2]
p D.instance_methods(false) # => []
 

メタプログラミングRubyのサンプルに少し書き足してみました。 カレントオブジェクトはDクラスのインスタンスですが、カレントクラスはCクラスであることがわかりますね。よってm2はCクラスのインスタンスメソッドになっています。

Classでクラスを開くことでカレントクラスを変えることができるのですが、この方法はカレントクラスにしたいクラスの名前がわからないと使えません。クラス名がわからない時に使うのが class_evalメソッドです。

 def add_method_to(a_class)
  a_class.class_eval do
    def m
      'Hello!'
    end
  end
end

add_method_to(String)
p "string".m # => 'Hello!'
 

このadd_method_toメソッドではクラスを引数にとってclass_evalを使うことで引数にとったクラスにカレントクラスを変更してからメソッドを定義しています。 class_evalは第四章のinstance_evalとは別物です。 instance_evalはselfを変更してブロック内を評価してくれるものでしたが、class_evalはselfに加えてカレントクラスも変更します。 スコープゲートを開いているわけではないのでスコープは変わりません。

クラスインスタンス変数

クラスのインスタンス変数とクラスのオブジェクトのインスタンス変数は別物です。

 class Myclass
  @my_var = 1

  def self.read
    @my_var
  end

  def write
    @my_var = 2
  end

  def read
    @my_var
  end
end

obj = Myclass.new
p obj.read # => nil
obj.write
p obj.read # => 2
p Myclass.read # => 1
 

ここでは2つの異なるオブジェクトである@my_varを定義しています。 クラスもオブジェクトの一つであるということを念頭において考えて行くのが大事になってきます。 最初にでてくる@my_varがクラスインスタンス変数。2つ目@my_varが今回だとobjオブジェクトのインスタンス変数ですね。 (ここメタプログラミングRubyでは1つ目の@my_varがobjのインスタンス変数で2つ目の@my_varがMyclassのクラスインスタンス変数だ、と書かれているのですが、コードの順番で行くと逆なので少しわかりにくかったです。説明の順番としては仕方ないのかもしれないですが。) クラスインスタンス変数はClassクラスに属しているオブジェクトのインスタンス変数というのが正しいです。MyclassはClassクラスのインスタンスなのでその中で定義されたものはクラスインスタンス変数となります。 クラスインスタンス変数はクラスしかアクセスできず、そのクラスのインスタンスからアクセスすることはできません。

特異メソッド

Stringに属したオブジェクトが全て大文字化かどうかをbooleanで返すメソッドをつくりたいが、特定の文字列にしかこのメソッドをもたせたくない、という場合。Refinementsを使って、特定のコードの前にusingを挿入するという方法もありますが、別の方法もあります。それが特異メソッドです。 同じクラスのオブジェクトでも、ある特定のオブジェクトにだけメソッドを追加したい、という場合に特異メソッドを使うことができます。

 str = "string"

def str.title?
  self.upcase == self
end

p str.title? # => false
p str.methods.grep(/title?/) # => [:title?]
p str.singleton_methods # => [:title?]
 

今までクラスに定義していたクラスメソッド

 Class Hoge
  def self.a_method
  end
end
 

これはクラスもオブジェクトであるという点からみれば

 Hoge = Class.new
def Hoge.a_method
end
 

という形とやっていることは同じです。つまりクラスメソッドはクラスの特異メソッドであるという事になります。

クラスマクロ

attt_*のようなメソッドをクラスマクロと呼ぶ。selfがクラスでも使えるのでクラスメソッドということですね。

特異クラス

特異メソッドがどこに定義されているのか?という問の答えが特異クラスです。

 obj = Object.new
p obj # => #<Object:0x00007f9fa893a068>
p obj.singleton_class # => #<Class:#<Object0x00007f9fa893a068>>

singleton_class = class << obj
  p self # => #<Class:#<Object:0x00007f9fa893a068>>
end
 

この例では<Class:#<Object:0x00007f9fa893a068>>がobjオブジェクトの特異クラスです。 class << objでも発見することができますが、singleton_classメソッドを使ったほうが楽でしょう。 オブジェクトのメソッド探索を行う際にはオブジェクトのクラスをみてそして更に親のクラスへとたどって行きますが、実は、オブジェクトのクラスのメソッドを見る前に、そのオブジェクトの特異クラスのメソッドを見にいっているのです。 クラスにもクラスはあるので、当然特異クラスがつくれます。

 class C
  class << self
    def a_class_method
      'C.a_class_method()'
    end
  end
end
 

これは要は

 class C
  def self.a_class_method
    'C.a_class_method()'
  end
end
 

と同じで、クラスメソッドはそもそもそのクラスの特異クラスに定義されています。

 p C.singleton_class.superclass # => #<Class:Object>
p C.superclass # => Object
 

このようにクラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスになります。

大統一理論

Rubyのオブジェクトのルールとして7つのルールが示されています。

  1. オブジェクトは一種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは一種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは一種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。 一種類しかないということは複数から同時に継承しないということです。下記の5にありますが、必ず親から子への一直線以外に継承はしていません。

  4. 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。 メタプログラミングRubyでのいわゆる「右へ」の移動で、本物のクラス、つまり特異クラスをもっているということですね。ほとんどのオブジェクトは特異クラスをもっています。true, false, nilは持っていません。(NilClass等はもっていますが。)

  5. すべてのクラスは(BasicObjectを除いて)一つの祖先(スーパークラスかモジュール)を持っている。つまりあらゆるクラスがBasicObjectに向かって1本の継承チェーンをもっている。 これと1,2,3のルールのおかげでメソッド探索はわかりやすいものになっていると思います。継承チェーンは二股に別れたりはしていません。

  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである

  7. メソッドを呼び出すときはRubyはレシーバーの本物のクラスに向かって右へ進み、継承チェーンを「上へ」進む。 この2つからなぜインスタンスがクラスメソッドにアクセス出来ないかを説明することができます。 クラスメソッドはそのクラスの特異クラスに定義されています。 Rubyのメソッド探索では一度だけしか右に行かないので、インスタンスからメソッド探索をおこなった場合、メソッドの特異クラスへと一度右に行った後はその特異クラスのスーパークラスであるobj.classが戻すクラスから上に探索していくので、そのスーパークラスの特異クラスまでは探索しません。よってスーパークラスの特異クラスにあるメソッドはインスタンスからはアクセスできないということになります。

Object#extend

モジュールをincudeするとモジュールのインスタンスメソッドのみがincludeしたクラスでインスタンスメソッドとして使えるようになりますが、 クラスの特異メソッドを開いて、その中でモジュールをincludeするとモジュールのインスタンスメソッドをクラスメソッドとして定義できます。 オブジェクトの特異クラスにも同様にでき、よく使われるので、Object#extendとしてメソッドが提供されています。

 module MyModule
  def my_method; p 'hello'; end
end

class MyClass
  extend MyModule
end

MyClass.my_method # => 'hello'
 

メソッドラッパー

アラウンドエイリアス

  1. メソッドにエイリアスをつける
  2. メソッドを再定義する
  3. 新しいメソッドから古いメソッドを呼び出す。 こうすることでエイリアスでは元のメソッドの内容が参照されるため、
 class String
  alias_method :origin_reverse!, :reverse!

  def reverse!
    upcase!
    origin_reverse!
  end
end

p "ruby".reverse! # => "YBUR"
 

エイリアスを定義することで、メソッドの再定義を行う時に、元のメソッドをエイリアスメソッドで呼び出して、ラップすることができます。 ここでは古いreverse!を新しいreverse!にラップしています。一種のモンキーパッチにも近い方法です。 新しいreverse!は古いreverse!の周囲(アラウンド)をラップしているのでこのトリックをアラウンドエイリアスをと呼びます。

Refinements

Refinementsを使ってsuperを呼び出すと元のメソッドを呼び出せます。

 module StringRefinements
  refine String do
    def reverse!
      upcase!
      super
    end
  end
end

using StringRefinements
p "python".reverse! # => "NOHTYP"
 

Prependラッパー

 module ExpilcitString
  def reverse!
    upcase!
    super
  end
end

String.class_eval do
  prepend ExpilcitString
end

p "go".reverse! # => "OG"
 

Prependは継承チェーンの下にモジュールが挿入されるのでメソッド探索の際にはprependされたモジュールにあるメソッドが先に使われる。

5章のクイズ

最初はsuperで解決しようとしたんですが、古い+に依存しているのでなかなか書けませんでした… メタプログラミングRubyのようにアラウンドエイリアスで解決するのが良さそうです。

まとめ

クラスメソッドがどこに定義されているのか?という問いに答えてくれる章でしたね。 ソースコードを読んでextendってなんだ?となることが多かったので、extendで特異クラスにメソッドを定義できるのは知っておきたいところ。 また特異クラスには特異クラスがあるので特異クラスの特異クラスの特異クラスとずっとつなげて行けるのでは…