Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rubyの private/protectedメソッドのあれこれ

cellotakです。

先日Rubyの privateメソッド、protectedメソッドについて調べるきっかけがあったのですが、意外と断片的な情報ばかりでまとまった情報があまりありませんでした。
そこで今回は自身の備忘録も兼ねて、細かめの仕様なども含めてprivate/protectedメソッド周りの情報をまとめてみました。

この記事にはprivate/protectedメソッドに関連する話題を1か所にまとめたいという意図があるので、だいぶ長文の上、実用的とはいえない情報も含まれていますので、その点ご留意ください。
また私自身理解しきれてないところもありますので、あくまで参考程度にして頂けますと幸いです。

ポイント

細かめの話になるので先にポイントをまとめます。

  • Rubyの private/protectedメソッドとは
    • privateメソッドは サブクラスでも呼び出せる。
    • privateメソッドは sendメソッドで呼び出せる。public_sendメソッドでは呼び出せない。
    • protectedメソッドが必要となるのはそれなりに特殊な場面
  • private/protected なクラスメソッド
    • クラスメソッドをprivateにしたい場合、インスタンスメソッドとは指定の仕方が異なるので注意。
    • クラスメソッドをprotectedにする方法はあるが、利用する場面があるとは考えづらく、実際にコードとして存在するのであれば他言語の仕様と勘違いされている可能性がある。
  • 色々な方法で呼び出す実験
    • インスタンスメソッドから private/protectedなクラスメソッドは呼び出せない
    • クラスメソッドから private/protectedなインスタンスメソッドは呼び出せない。

Rubyのprivate/protectedメソッドとは

privateメソッド

Module#privateを利用することで、外部から利用できないようにすることができます。
以下の例では #private_instance_method は直接呼び出すと失敗していますが、Hogeクラスの#call_private_instance_methodというインスタンスメソッドからは呼び出せています。

class Hoge
  def public_instance_method
    'public_instance_methodが呼ばれた'
  end

  def call_private_instance_method
    private_instance_method
  end

  private

  def private_instance_method
    'private_instance_methodが呼ばれた'
  end
end


# publicメソッドを呼び出した場合 → 呼び出し成功
> Hoge.new.public_instance_method
=> "public_instance_methodが呼ばれた"

# privateメソッドを直接呼び出した場合 → 呼び出し失敗
> Hoge.new.private_instance_method
NoMethodError: private method 'private_instance_method' called for #<Hoge:0x00007f88f957c0e0>
Did you mean?  private_methods
from (pry):26:in '__pry__'

# メソッド経由で呼び出した場合 → 呼び出し成功
> Hoge.new.call_private_instance_method
=> "private_instance_methodが呼ばれた"

ただし、Javaのprivateメソッドなどとは違いサブクラスでも呼び出せます。

class Fuga < Hoge
  #Hogeクラスを継承したFugaクラスからもHogeクラスのprivateメソッドを呼び出せる
  def call_private_instance_method
    private_instance_method
  end
end

# Fugaクラスから呼び出した場合 → 呼び出し成功
> Fuga.new.call_private_instance_method

=> "private_instance_methodが呼ばれた"

ちなみに、Rubyのprivateメソッドは元々の定義としては「レシーバを明示的に指定して実行できないメソッド」であり、自クラス内で呼び出すときはselfというレシーバを省略可能であるから、実質他クラスから実行できなくなっているという理屈になります。

Ruby2.7以降ではselfだけはレシーバとして明示的に指定できるようになったので、元の定義は崩れつつあり「self以外のレシーバを明示的に指定して実行できないメソッド」
となっています。

※さらに細かいことを言うと、Ruby2.7未満であっても =付きのメソッドでselfというレシーバを用いることはできていたみたいです。)

参照: Ruby 2.7 の変更点 - self でのプライベートメソッド呼び出し
参照: Ruby 2.7 allows calling a private method with self.

class Hoge
  def call_private_instance_method
    self.private_instance_method # ←Ruby2.7未満ではこの形で呼び出せない。それ以降は呼び出せる。
  end

  private

  def private_instance_method
    'private_instance_methodが呼ばれた'
  end
end

#send, #public_send からの呼び出し

実は privateメソッドは #sendメソッドからだと呼び出せてしまうのでご注意ください。
public メソッドだけ呼び出せれば良い場合は#public_sendを使う方がよさそうです。

class Hoge
  def public_instance_method
    'public_instance_methodが呼ばれた'
  end

  def call_private_instance_method
    private_instance_method
  end

  private

  def private_instance_method
    'private_instance_methodが呼ばれた'
  end
end

# privateメソッドをsendメソッド経由で呼び出した場合 → 呼び出し成功
> Hoge.new.send(:private_instance_method)
=> "private_instance_methodが呼ばれた"

# privateメソッドをpublic_sendメソッド経由で呼び出した場合 → 呼び出し失敗
> Hoge.new.public_send(:private_instance_method)
NoMethodError: private method 'private_instance_method' called for an instance of Hoge
from (pry):5:in 'public_send'

# publicメソッドをpublic_sendメソッド経由で呼び出した場合 → 呼び出し成功
Hoge.new.public_send(:public_instance_method)
=> "public_instance_methodが呼ばれた"

protected メソッド

Module#protectedを利用することで、外部から利用を制限できます。

るりまことRuby リファレンスマニュアルでは以下のように説明されています。

protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。
クラス/メソッドの定義 (Ruby 2.1.0)より

これだけではわかりづらいのでサンプルコードを用意しました。

class Hoge
  def call_protected_instance_method(other_hoge)
    protected_instance_method
    # self以外の同クラスインスタンスに対してprotected_instance_methodが呼べる
    other_hoge.protected_instance_method
  end

  protected

  def protected_instance_method
    'protected_instance_methodが呼ばれた'
  end
end

# protectedメソッドを外部から直接呼び出した場合 → 呼び出し失敗
> Hoge.new.protected_instance_method
NoMethodError: protected method 'protected_instance_method' called for #<Hoge:0x00007f88f93f7170>
Did you mean?  call_protected_instance_method
from (pry):39:in '__pry__'

# メソッド経由で呼び出し、引数にHogeクラスの他インスタンスを渡した場合 → 呼び出し成功
> Hoge.new.call_protected_instance_method(Hoge.new)
protected_instance_methodが呼ばれた # 元のインスタンスに対する呼び出し
protected_instance_methodが呼ばれた # 引数で引いた他のインスタンスに対する呼び出し
=> nil

# メソッド経由で呼び出し、引数に他クラス(Fuga)のインスタンスを渡した場合 → 呼び出し失敗
> Hoge.new.call_protected_instance_method(Fuga.new) # 他クラスのインスタンスを渡した場合
protected_instance_methodが呼ばれた
NoMethodError: undefined method `protected_instance_method' for an instance of Fuga
from /app/app/models/hoge.rb:8:in `call_protected_instance_method'

外部から呼び出したときの挙動としてはprivateメソッドと一緒です。
上記の例でいうと、Hoge.new.protected_instance_methodのようにして直接外部からは呼ぶことはできません。

一方、#call_protected_instance_methodメソッド経由では挙動が変わってきます。
キーとなるのは

other_hoge.protected_instance_method

という部分で、引数として渡しているother_hoge#protected_instance_methodのレシーバになっていますが、このother_hogeHogeクラスのインスタンスであれば呼び出し成功、そうでなければ呼び出しが失敗します。
privateメソッドの場合、先ほどの説明通りself以外のレシーバは指定できないため、other_hoge.private_instance_methodのような呼び出しは必ず失敗しますが、protectedメソッドの場合は、同クラスインスタンスであればレシーバとして指定することが可能となります。

以上を踏まえると
「メソッド定義内で、同クラスの他インスタンスに対して呼びたいメソッドがあるので privateにはできない、しかしpublicにしてしまうとオープン過ぎて困る」
という場面がこの protectedメソッドの使いどころのようです。

private と protected の使い分けに関する Matz さんの分かりやすい説明」によるとMatz さんが以下のように解説してくれていたそうです。

つまり,privateは自分からしか見えないメソッドであるのに対し
て,protectedは一般の人からは見られたくないが,仲間(クラスが
同じオブジェクト)からは見えるメソッドです.
protectedは例えば2項演算子の実装にもう一方のオブジェクトの状
態を知る必要があるか調べる必要があるが,そのメソッドをpublic
にして,広く公開するのは避けたいというような時に使います.
Ruby の private と protected。歴史と使い分け #Ruby - Qiitaの引用文より

大体認識は合ってそうです。

実用的な例

上記内容を社内で発表してみたところ、以下のコード例とともに「Matzさんの説明の通り、#== みたいなメソッドを実装するときに使うのではないでしょうか?」とのコメントを頂きました。

class X
  def initialize(id)
    @id = id
  end

  def ==(other)
    id == other.id # 呼べる
  end

  protected

  def id
    @id
  end
end

X.new(10) == X.new(10) # true
X.new(10) == X.new(20) # false
X.new(10).id # NoMethodError

この例はかなり実用的でわかりやすそうです。
#id 自体はpublicにしたくないけど、#== を実装するにはレシーバと引数で引いたインスタンス両方の #id が必要になるので、privateにする訳にもいかないというシチュエーションがよく再現されています。

こうしてみてみるとWebアプリケーションを実装する際にこういったコードを書く場面はかなり少なそうな気もします。

補足

  • Javaだとprotectedメソッドは「クラス内、同一パッケージ、サブクラスからアクセス可」というものらしいので、ここもRubyは定義が異なります。
    「Javaのprotected ≒ Rubyのprivate」
    みたいな感じでしょうか。

参照 : Ruby の private と protected。歴史と使い分け

  • privateメソッド同様 protectedメソッドはサブクラスからも呼び出せます。

private/protected なクラスメソッド

ここまで説明してきた privateメソッドやprotectedメソッドはインスタンスメソッドについての話でしたが、クラスメソッドを外部から隠したい場合もあります。その場合はどうなるのでしょうか。

privateなクラスメソッド

クラスメソッドをprivateにしたい場合、インスタンスメソッドと同じようにModule#privateを利用した指定はできません。
以下のように書くと、privateにしたつもりでも出来ておらず、publicな状態となってしまいます。

class Hoge
  private

  def self.private_class_method # 外部から呼び出せてしまう
    'private_class_methodが呼ばれた'
  end
end

# privateなはずなのに、外部から呼び出し成功してしまう。
> Hoge.private_class_method
=> "private_class_methodが呼ばれた"

解決策

クラスメソッドをprivateにしたい場合は、以下2つのいずれかの方法でprivate指定する必要があります。

  • Module#private_class_methodを使う
    #private_class_methodを使えばprivate指定ができます。
class Hoge
  def self.private_hoge_class_method
    'private_class_methodが呼ばれた'
  end

   private_class_method :private_hoge_class_method
end

# private化されて外部からの呼び出しが失敗する
> Hoge.private_hoge_class_method
NoMethodError: private method 'private_hoge_class_method' called for Hoge:Class
Did you mean?  private_class_method
from (pry):60:in '__pry__'
  • 特異クラスを作ってprivate指定をする
    特異クラスを作ってその中でprivate指定をすることでもprivate化できます。
class Hoge
  class << self
    private

    def private_hoge_class_method
      'private_class_methodが呼ばれた'
    end
  end
end

# private化されて外部からの呼び出しが失敗する
> Hoge.private_hoge_class_method
NoMethodError: private method 'private_hoge_class_method' called for Hoge:Class
Did you mean?  private_class_method
from (pry):69:in '__pry__'

確証はないですが、どちらの方法を使っても特に挙動に違いはなさそうです。

protectedなクラスメソッド

privateメソッドと同様に Module#protectedを利用した protected指定はできません。

class Hoge
  protected

  def self.protected_class_method # 外部から呼び出せてしまう。
    'protected_class_methodが呼ばれた'
  end
end

# protectedなはずなのに、外部から呼び出し成功してしまう。
> Hoge.protected_class_method
=> "protected_class_methodが呼ばれた"

解決策

こちらはModule#protected_class_methodのようなメソッドは存在しないので特異クラスを作ってprotectedにする方法しかありません。

class Hoge
  class << self
    protected

    def protected_hoge_class_method
      'protected_class_method'
    end
  end
end

# protected化されて外部からの呼び出しが失敗する
> Hoge.protected_hoge_class_method
NoMethodError: protected method 'protected_hoge_class_method' called for Hoge:Class
Did you mean?  protected_methods
from (pry):77:in '__pry__'

しかし、そもそもの話ですが protectedなクラスメソッドに関しては使う場面がなさそうな気がします。
使われているとしたら「このメソッドはサブクラスでも利用したいからprotectedにしよう」という感じで他言語の仕様と勘違いして使われている可能性がありそうです。

色々な方法で呼び出す実験

実際使う場面があるかどうかは考えず、色々な呼び出し方をした場合どうなるかの実験をしてみました。
サブクラスでどうなるかを確かめ始めたらキリがなさそうだったので省略しました。

結果まとめ

先に結果をまとめた表を示します。

呼び出し元 呼び出し先 結果
インスタンスメソッド privateインスタンスメソッド
protectedインスタンスメソッド
privateクラスメソッド ×
protectedクラスメソッド ×
クラスメソッド privateクラスメソッド
protectedクラスメソッド
privateインスタンスメソッド ×
protectedインスタンスメソッド ×

では詳しく見ていきます。

インスタンスメソッドからprivateインスタンスメソッドを呼び出すと

当然呼び出せます。普通のprivateなインスタンスメソッドです。

class Hoge
  def call_private_instance_method_from_instance_method #呼び出せる
    private_instance_method
  end

  private

  def private_instance_method
    'private_instance_methodが呼ばれた'
  end
end

# 呼び出し成功
> Hoge.new.call_private_instance_method_from_instance_method
=> "private_instance_methodが呼ばれた"

インスタンスメソッドからprotectedインスタンスメソッドを呼び出すと

これも当然呼べます。通常想定されているprotectedメソッドの呼ばれ方です。
(下記コードはprotectedである意味はない例ですが)

class Hoge
  def call_protected_instance_method_from_instance_method #これは呼べる
    protected_instance_method
  end

  protected

  def protected_instance_method
    'protected_instance_methodが呼ばれた'
  end
end

# 呼び出し成功
> Hoge.new.call_protected_instance_method_from_instance_method
=> "protected_instance_methodが呼ばれた"

インスタンスメソッドからprivateクラスメソッドを呼び出すと

レシーバに self.classとか Hogeのようにself以外を指定する必要がでてくるので呼び出せません。

class Hoge
  def call_private_class_method_from_instance_method #これは呼べない
    self.class.private_class_method
  end

  class << self
    private

    def private_class_method
      'private_class_methodが呼ばれた'
    end
  end
end

# 呼び出し失敗
> Hoge.new.call_protected_instance_method_from_instance_method
NoMethodError: undefined method `call_protected_instance_method_from_instance_method' for an instance of Hoge
from (pry):1:in `__pry__'

インスタンスメソッドからprotectedクラスメソッドを呼び出すと

この場合呼び出せません。privateではないのでレシーバが明示的に指定できないといった話は関係なさそうですが、るりまにあったprotectedの定義

protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。
クラス/メソッドの定義 (Ruby 2.1.0)より

から考えると、インスタンスから呼び出すとselfはインスタンスになり、「そのメソッドを持つオブジェクトが selfであるコンテキスト」から外れるのではないかと推察してます。(これに関しては本当かどうかが怪しい)

class Hoge
  def call_protected_class_method_from_instance_method # これは呼べない
    self.class.protected_class_method
  end

  class << self
    protected

    def protected_class_method
      'protected_class_method'
    end
  end
end

# 呼び出し失敗
> Hoge.new.call_protected_class_method_from_instance_method
NoMethodError: protected method `protected_class_method' called for class Hoge
from /app/app/models/hoge.rb:3:in `call_protected_class_method_from_instance_method'

クラスメソッドからprivateクラスメソッドを呼び出すと

これは当然呼び出せます。普通のprivateなクラスメソッドの使われ方です。

class Hoge
  def self.call_private_class_method_from_class_method #これは呼べる
    private_class_method
  end

  class << self
    private

    def private_class_method
      'private_class_methodが呼ばれた'
    end
  end
end

# 呼び出し成功
> Hoge.call_private_class_method_from_class_method
=> "private_class_methodが呼ばれた"

クラスメソッドからprotectedクラスメソッドを呼び出すと

そもそも protectedなクラスメソッドは使わないのではという議論はさておいて、呼び出せます。

class Hoge
  def self.call_protected_class_method_from_class_method #これは呼べる
    protected_class_method
  end

 class << self
   protected

   def protected_class_method
     'protected_class_methodが呼ばれた'
   end
 end
end

# 呼び出し成功
> Hoge.call_protected_class_method_from_class_method
=> "protected_class_methodが呼ばれた"

クラスメソッドからprivateインスタンスメソッドを呼び出すと

レシーバに self.newというようにインスタンスを指定する必要がでてくるので呼び出せません。

class Hoge
  def self.call_private_instance_method_from_class_method #呼び出せない
    self.new.private_instance_method
  end

  private

  def private_instance_method
    'private_instance_methodが呼ばれた'
  end
end

# 呼び出し失敗
> Hoge.call_private_instance_method_from_class_method
NoMethodError: private method `private_instance_method' called for an instance of Hoge
from /app/app/models/hoge.rb:3:in `call_private_instance_method_from_class_method'

クラスメソッドからprotectedインスタンスメソッドを呼び出すと

呼び出せません。インスタンスメソッドからprotectedクラスメソッドを呼び出した場合と同じで、クラスメソッドで呼び出すとselfはクラスになってしまうので、「そのメソッドを持つオブジェクトが selfであるコンテキスト」から外れるのではないかと推察してます。

class Hoge
  def self.call_protected_instance_method_from_class_method #これは呼べない。
    self.new.protected_instance_method
  end

  protected

  def protected_instance_method
    'protected_instance_methodが呼ばれた'
  end
end

# 呼び出し失敗
> Hoge.call_protected_instance_method_from_class_method
NoMethodError: protected method `protected_instance_method' called for an instance of Hoge
from /app/app/models/hoge.rb:3:in `call_protected_instance_method_from_class_method'

実験の背景

なぜこんなことを確かめていたかというと、「インスタンスメソッドからprotectedなクラスメソッドを呼び出そうとしている(実際はModule#protectedを使用していたせいで実質publicになっていた)」コードを見つけたので、「じゃあちゃんとprotectedにしてあげたらどうなるんだ?」というのを確かめたかったからです。

class Hoge
  def call_protected_method
    Hoge.protected_class_method_from_instance_method
  end

  protected #この指定は実質無効だったので成り立っていた。

  def self.protected_class_method
    'protected_class_method'
  end
end

結局、protectedなクラスメソッドをインスタンスメソッドから呼び出すのは不可ということが分かったので、この場合protected指定すること自体がおかしいですねというオチでした。

最後に

以上 private/protectedメソッドについてまとめてみました。
他にも関連する情報見つけ次第更新していければと思います。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。