Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのクラス・インスタンス・メタクラスを徹底解説 ― メソッド定義の仕組みを理解する

Ruby Magicの新しいエピソードへようこそ! 今月のテーマはメタクラスです。二人の開発者による議論がきっかけで生まれたこのテーマ(こんにちは、Maudさん!)について、じっくり掘り下げていきます。

メタクラスを調べることで、Rubyにおけるインスタンスメソッドとクラスメソッドの動作原理が見えてきます。さらに、明示的なレシーバを渡してメソッドを定義する方法と、class << selfinstance_eval を使う方法との違いも理解できるでしょう。それでは始めましょう!

クラスインスタンスとインスタンスメソッド

Rubyでなぜメタクラスが必要なのかを理解するために、まずはインスタンスメソッドとクラスメソッドの違いから確認していきます。

Rubyにおいて、クラスとは他のオブジェクトを生成するための設計図を定義するオブジェクトです。クラスは、そのクラスのすべてのインスタンスで利用可能なメソッドを決定します。

クラスの中でメソッドを定義すると、そのクラスにインスタンスメソッドが作られます。その後生成されるすべてのインスタンスは、このメソッドを利用できます。

class User
  def initialize(name)
    @name = name
  end
 
  def name
    @name
  end
end
 
user = User.new('Thijs')
user.name # => "Thijs"

この例では、Userというクラスを作り、ユーザーの名前を返すインスタンスメソッド#nameを定義しています。続いてこのクラスを使ってクラスインスタンスを生成し、変数userに格納しました。userUserクラスのインスタンスなので、#nameメソッドを呼び出せます。

クラスは自身のインスタンスメソッドをメソッドテーブルに格納します。各インスタンスは自分の属するクラスのメソッドテーブルを参照することで、インスタンスメソッドへアクセスするのです。

クラスオブジェクト

クラスメソッドとは、インスタンスを生成しなくてもクラスに対して直接呼び出せるメソッドのことです。定義時にメソッド名の先頭にself.を付けることで作成できます。

実は、クラス自体もオブジェクトです。定数がクラスオブジェクトを参照しているため、そこに定義されたクラスメソッドはアプリケーションのどこからでも呼び出せます。

class User
  # ...
 
  def self.all
    [new("Thijs"), new("Robert"), new("Tom")]
  end
end
 
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

self.を付けて定義されたメソッドは、クラスのメソッドテーブルには追加されません。代わりに、クラスのメタクラスに追加されます。

メタクラスとは何か

Rubyでは、クラス以外のすべてのオブジェクトにも隠れたメタクラスが存在します。メタクラスはシングルトン、つまり単一のオブジェクトだけに属するものです。同じクラスから複数のインスタンスを作っても、それらは同じクラスを共有しますが、それぞれ別々のメタクラスを持ちます。

thijs, robert, tom = User.all
 
thijs.class # => User
robert.class # => User
tom.class # => User
 
thijs.singleton_class  # => #<Class:#<User:0x00007fb71a9a2cb0>>
robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>>
tom.singleton_class    # => #<Class:#<User:0x00007fb71a9a2c10>>

この例では、3つのオブジェクトはいずれもUserクラスに属していますが、それぞれのシングルトンクラス(メタクラス)のオブジェクトIDが異なることから、独立したオブジェクトであることが分かります。

メタクラスがあるおかげで、Rubyでは既存のオブジェクトに直接メソッドを追加できます。ただし、これはオブジェクトのクラスに新しいメソッドを追加するわけではありません。

robert = User.new("Robert")
 
def robert.last_name
  "Beekman"
end
 
robert.last_name # => "Beekman"
User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>)

ここでは、変数robertに格納されたユーザーオブジェクトに#last_nameメソッドを追加しています。robertUserのインスタンスですが、新しく作ったUserのインスタンスには#last_nameメソッドは存在しません。このメソッドはrobertのメタクラスにしか定義されていないからです。

selfとは何か?

メソッドを定義するときにレシーバを渡すと、新しいメソッドはクラスのメソッドテーブルではなく、そのレシーバのメタクラスに追加されます。

tom = User.new("Tom")
 
def tom.last_name
  "de Bruijn"
end

上の例では、メソッド定義時にtomをレシーバとして渡すことで、#last_nametomオブジェクトに直接追加しています。

実は、クラスメソッドもまったく同じ仕組みで動いています。

class User
  # ...
 
  def self.all
    [new("Thijs"), new("Robert"), new("Tom")]
  end
end

ここでは、.allメソッドを定義する際に明示的にselfをレシーバとして渡しています。クラス定義内ではselfはそのクラス自身(この場合はUser)を指すため、.allメソッドはUserのメタクラスに追加されるのです。

Userは定数に格納されたオブジェクトなので、参照するたびに必ず同じオブジェクト――そして同じメタクラス――にアクセスすることになります。

メタクラスを開く

これまでの学習で、クラスメソッドとは「クラスオブジェクトのメタクラスに定義されたメソッド」であることが分かりました。この知識をもとに、皆さんがどこかで目にしたことのある、クラスメソッドを作るための別の手法を見ていきましょう。

class << self

近年はやや使われなくなりましたが、一部のライブラリではclass << selfを使ってクラスメソッドを定義しています。この構文は、現在のクラスのメタクラスを開き、直接操作するためのトリックです。

class User
  class << self
    self # => #<Class:User>
 
    def all
      [new("Thijs"), new("Robert"), new("Tom")]
    end
  end
end
 
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

この例では、Userのメタクラスにメソッドを追加することで、クラスメソッドUser.allを作っています。先ほどのようにメソッドごとにレシーバを明示的に渡す代わりに、selfUserそのものではなくUserのメタクラスに切り替えている点がポイントです。

前述のとおり、明示的なレシーバなしで定義されたメソッドは、現在のクラスのインスタンスメソッドとして追加されます。このブロック内では、現在のクラスはUserのメタクラス(#<Class:User>)になっているのです。

instance_eval

もう一つの選択肢がinstance_evalです。これも同様の働きをしますが、大きな違いが一つあります。ブロック内で定義されたメソッドはクラスのメタクラスに追加されますが、selfは引き続き元のクラスを参照し続けるのです。

class User
  instance_eval do
    self # => User
 
    def all
      [new("Thijs"), new("Robert"), new("Tom")]
    end
  end
end
 
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

この例でも、先ほどと同じようにUserのメタクラスにインスタンスメソッドを定義していますが、selfは依然としてUserを指しています。通常は同じオブジェクトを指しますが、「デフォルトのdefinee」とselfが異なるオブジェクトを指すこともある、というわけです。

まとめ

今回学んだことを整理しましょう。メソッドを持てるのはクラスだけであり、インスタンスメソッドとは実際にはオブジェクトのメタクラス上のメソッドであるということ。class << selfは単純にselfを差し替えてメタクラス上にメソッドを定義できるようにするものであり、instance_evalもほぼ同じことをします(ただしselfには触れません)。

日常的な開発でメタクラスを明示的に扱うことは少ないかもしれませんが、Rubyは内部でメタクラスを多用しています。メソッドを定義したときに内部で何が起きているのかを知っていれば、Rubyがなぜそのように振る舞うのか(そしてなぜクラスメソッドにself.を付ける必要があるのか)が理解できるようになるでしょう。

最後までお読みいただきありがとうございました。この記事が気に入った方は、Ruby Magicを購読すれば、月1回程度公開される新しい記事をお知らせメールでお受け取りいただけます。

  1. Rubyでカスタム例外を作成する方法【初心者向けガイド】

    Rubyでは、独自の例外クラスを簡単に作成できます。この記事では、カスタム例外を作るための基本的な手順を、コード例とともにわかりやすく解説します。 1. 新しいクラスを作成する Rubyにおいて、例外も他のオブジェクトと同じくクラスとして定義されています。新しい種類の例外を作りたい場合は、StandardError、あるいはそのサブクラスを継承したクラスを定義するだけでOKです。 class MyError < StandardError end raise MyError 慣例として、新しく作る例外クラスの名前は「Error」で終わるように命名します。また、カスタム例外をモジュ

  2. なぜクラスを作るのか?抽象化の重要性と良いクラス設計の基本

    前回の記事「なぜnilを使うのか」に続き、普段当たり前と思って使っているものについても、同じ問いを立ててみるのは良いアイデアではないでしょうか。 例えば… 巨大で醜いコードの塊を1つ作るのではなく、なぜ私たちはクラスを使うのでしょうか? 基本的なレベルで言えば、コードとデータを論理的な単位に整理するためにクラスを使います。しかし、それだけではありません。 クラスを使うと、抽象化(abstraction)を作り出せるのです。 抽象化とは何か? 実は、私たちは毎日、毎分、抽象化を利用しています。 今この記事を書いているキーボードは、次のようなもので構成されています。 ケーブル Cherry MX