特異メソッド
特異メソッドは、定義すれば滅多に使われないようにみえて、実は「クラスメソッド」という形で多用されている。
クラスメソッドは「Classクラス」にメソッドを定義して実装しない限り、すべて、「Classクラスのインスタンス」の特異メソッドである。 特異メソッドの定義は
def object.method ; end def Class.method: end
どちらでも可能で、前者がインスタンスに対する特異メソッド、後者がクラスに対する特異メソッドの定義になる。 「クラス定義」の中で使えるクラスメソッドのことを「クラスマクロ」と呼ぶ。
クラスマクロの使用例として、古い呼び方のメソッド呼び出しから新しい名前のメソッドを呼び出すプログラムが掲載されている。
class Book def title puts "Rubyのすべて" end def lend_to(user) puts "Lend to #{user}" end def self.deprecated(old_method, new_method) define_method(old_method) do |*args, &block| warn "Warning: #{old_method}() is deprecated. Use #{new_method}()" send(new_method, *args, &block) end end deprecated :GetTitle, :title deprecated :LEND_TO_USER, :lend_to deprecated :title2, :subtitle end
[3] pry(main)> require "./deprecated.rb" /home/pyons/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/pry-0.12.2/lib/pry/pager.rb:150: warning: Insecure world writable dir /home/pyons/.rbenv/versions in PATH, mode 040777 => true [4] pry(main)> b = Book.new => #<Book:0x00007ffff2567148> [5] pry(main)> b.LEND_TO_USER("SHOUHEI") Warning: LEND_TO_USER() is deprecated. Use lend_to() Lend to SHOUHEI => nil
この例では、クラス定義の中でdepereceted
というクラスマクロを定義し、各インスタンスにdeprecetedのメソッドに対して警告を発するメソッドを与えている。
では、特異メソッドは通常どこに住んでいるものなのか。Rubyではメソッドは本来クラスに属しているものだった。
特異メソッドがクラスに所属してしまうと、そのクラスから生成された全てのインスタンスがそのメソッドを持つことになる。
オブジェクトには住めない。オブジェクトはあくまで、インスタンス変数とクラスへのリンクしかもっていない。
答えはオブジェクトの裏には「特異クラス」というものが存在し、実はそこに住んでいる。そのスコープにアクセスするには、以下のようなコードを用いる。
string = "Hello World" def string.shibuya puts "Shibuya is busy city." end singleton_class = class << string self end p singleton_class # #<Class:#<String:0x00007fffda2f7538>> p singleton_class.class # Class p string.singleton_class # #<Class:#<String:0x00007fffda2f7538>> p singleton_class.instance_methods(false) # [:shibuya] p singleton_class.superclass # String
これで、特異クラスの正体が明らかになるとともに、特異クラスが特異メソッドを持っているこのがわかった。 ここで扱ったのは「オブジェクト」に対する特異メソッドである。先ほど、クラスメソッドが「Classクラスのインスタンス」に対する特異メソッドであることを理解した。
クラスメソッドに特異クラスを追加すると、継承先のクラスでもそのメソッドがクラスメソッドとして扱えるのに気づく。
class Train class << self def a_class_method puts "This is class method of Train" end end end class Subway < Train end p Subway.a_class_method # This is class method of Train
特異メソッドは「そのインスタンス」にしか存在しないメソッドのはずだから、これは不自然である。
「インスタンスに対する特異メソッドの定義」であれば、インスタンスの生成元クラスとインスタンスの間に「特異クラス」という特別なクラスが挟まり、クラスは特異クラスを介して生成元のクラスと繋がっていると考えることが出来た。
そこで、クラスメソッドに関しても同じような図式を用いると以下のようになる。
これだけ見ると、「まあTrain Classを継承しているなら、Singleton_methodも使えるかな」と思ってしまったなら、メソッド探索の基礎が完全に抜けている。
メソッド探索は「生成元のクラスに戻って、そのスーパークラスを辿る」であった。このSubwa ClassはTrain Classを「継承」しているが、インスタンスとしては「Class class」が生成元である。最初にメソッド探索が行われるのは当然「Class class」である。
という訳で、Subway ClassがTrain Classの特異メソッドを利用できるのであれば、Subway ClassとClass classの間に別の生成元クラスがあり、そいつが#Train Classを継承していなければ、メソッド探索上に特異メソッドが現れない、ということが言える。
こうなってれば、Train Classの特異メソッドをSubway Classから利用できる。
そして、実際にRubyにはSubway Classにも特異クラスが存在する。特異クラスはTrain Classの特異クラス(#Train Class)を継承しているので、「生成元クラスから遡上する」というメソッド探索経路上で、Train Classの特異メソッドと出会う。
この説明により、「クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである。」という説明が理解できる。(下図)そして、こうした実装になっている理由は「親クラスの特異クラスで定義した特異メソッドを、子クラスの特異クラスメソッドとしても利用可能にするため」という利便性の向上のためであったと理解できる。
ここで、「Class_evalにはクラスに対する新しいメソッドの追加が可能で、instance_evalにはクラスに対する新しいメソッドの追加が出来ない」という特性の理由を明らかにすることが出来る。
instance_evalの場合はselfを指定したクラス内やオブジェクト内に指定できるのが売りだった。しかしこれでは、クラスに対する新しいメソッドの追加は出来ない。
Class定義 - Pyons Tech Blog
instance_evalはそのブロック内で、selfを「レシーバーの特異クラス」に設定する。新しいメソッドが追加されるのが、特異クラスであれば、「オブジェクト」に対するinstance_eval
は、当然インスタンスに対する特異メソッドの定義になる。
一方クラスに対するinstance_eval
は、クラスの特異クラスがselfとなるため、「新しいクラスメソッドの定義」になる。前回の例を用いて、クラスメソッドが定義されているか再確認してみる。
class MyClass end MyClass.instance_eval do def show_number return "MyClassの特異メソッドですよー。" end end p MyClass.show_number # "MyClassの特異メソッドですよー。" singleton_class = class << MyClass self end p singleton_class # #<Class:MyClass>
やっぱり、クラスに対して特異クラスを定義することで、クラスメソッドを定義することが出来ている。
ここで一つ釈然としないのは「オブジェクトの特異クラスを取得するための構文では、クラスの特異クラスを取得できない。」という点である。これがちょっと気になっている。
最後に「オブジェクト」ではなく「クラス」に対してアクセサを定義する方法を考えてみる。
class MyClass class << self # この記法は最もスタンダードなクラスメソッドの定義 attr_accessor :class_variable end end MyClass.class_variable = 100 p MyClass.class_variable # 100
今度はモジュールを使って、クラスメソッドを定義してみる。
module MyModule def myclass_method puts "I want to make this method class method!" end end class MyClass class << self include MyModule end end MyClass.myclass_method # I want to make this method class method!
こんな感じで利用できる。ここまでは、クラスの特異クラスを定義して、新たなクラスメソッドを定義する方法を見てきたが、再びインスタンスに特異クラスを定義する方法をみていく。
通常のインスタンスに特異クラスを定義することを「オブジェクト拡張」と呼び、これを行うのに便利なメソッドがRubyで用意されている。クラスの拡張にも同じものが使える。
module MyModule def my_method puts "object extended!" end end object = Object.new class << object include MyModule end object.my_method # object extended!
これは通常のオブジェクト拡張の方法で、モジュールを利用して拡張させるのは、ありがちなようだ。
module MyModule def my_method puts "object extended!" end end ## Object拡張 object = Object.new object.extend(MyModule) object.my_method # object extended! ## クラス拡張 class MyClass end MyClass.extend(MyModule) MyClass.my_method # object extended!
メソッドextend
はこうしたモジュールによる拡張を簡単に行うためのメソッドで、クラスに対してもインスタンスに対しても、同じように用いることが出来る。
alias_method について
alias_methodはメソッドに別名を付けるメソッドだが、挙動に注意する必要がある。
class String alias_method :real_length, :length # 新名称 # 旧名称 def length real_length > 5 ? "long" : "short" end end p "012345".length # "long" p "012345".real_length # 6
alias_method
は新名称を第一引数に、旧名称を第二引数にとる。
なので、real_length
メソッドを使えば、Stringクラスの再オープンで定義されている方のメソッドが呼ばれそうなものだが、実際にはStringクラスのオリジナルなメソッドが呼ばれた。この辺の挙動はテストで問われてもおかしくないので、しっかり覚えておきたい。
メソッドラッパー
メソッドの中にメソッドをラップする方法として、alias_method
を用いた方法と、この他にもう2パターンが紹介されている。