メタプログラミング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つのルールが示されています。
- オブジェクトは一種類しかない。それが通常のオブジェクトかモジュールになる。
- モジュールは一種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
メソッドは一種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。 一種類しかないということは複数から同時に継承しないということです。下記の5にありますが、必ず親から子への一直線以外に継承はしていません。
全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。 メタプログラミングRubyでのいわゆる「右へ」の移動で、本物のクラス、つまり特異クラスをもっているということですね。ほとんどのオブジェクトは特異クラスをもっています。true, false, nilは持っていません。(NilClass等はもっていますが。)
すべてのクラスは(BasicObjectを除いて)一つの祖先(スーパークラスかモジュール)を持っている。つまりあらゆるクラスがBasicObjectに向かって1本の継承チェーンをもっている。 これと1,2,3のルールのおかげでメソッド探索はわかりやすいものになっていると思います。継承チェーンは二股に別れたりはしていません。
オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである
- メソッドを呼び出すときは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'
メソッドラッパー
アラウンドエイリアス
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で特異クラスにメソッドを定義できるのは知っておきたいところ。 また特異クラスには特異クラスがあるので特異クラスの特異クラスの特異クラスとずっとつなげて行けるのでは…