特異メソッド

特異メソッドは、定義すれば滅多に使われないようにみえて、実は「クラスメソッド」という形で多用されている。

クラスメソッドは「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

特異メソッドは「そのインスタンス」にしか存在しないメソッドのはずだから、これは不自然である。

インスタンスに対する特異メソッドの定義」であれば、インスタンスの生成元クラスとインスタンスの間に「特異クラス」という特別なクラスが挟まり、クラスは特異クラスを介して生成元のクラスと繋がっていると考えることが出来た。

そこで、クラスメソッドに関しても同じような図式を用いると以下のようになる。

f:id:Pyons:20191011165148j:plain:w300

これだけ見ると、「まあ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から利用できる。

f:id:Pyons:20191012115721j:plain:w300

そして、実際にRubyにはSubway Classにも特異クラスが存在する。特異クラスはTrain Classの特異クラス(#Train Class)を継承しているので、「生成元クラスから遡上する」というメソッド探索経路上で、Train Classの特異メソッドと出会う。

この説明により、「クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである。」という説明が理解できる。(下図)そして、こうした実装になっている理由は「親クラスの特異クラスで定義した特異メソッドを、子クラスの特異クラスメソッドとしても利用可能にするため」という利便性の向上のためであったと理解できる。

f:id:Pyons:20191012114504j:plain:w500

ここで、「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パターンが紹介されている。