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_hoge
がHoge
クラスのインスタンスであれば呼び出し成功、そうでなければ呼び出しが失敗します。
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メソッドについてまとめてみました。
他にも関連する情報見つけ次第更新していければと思います。