Rubyのリファレンスマニュアル(バージョンは2.1.0)によれば、Rubyの呼び出し制限(英語ではAccess Control)には、publicとprivate、protectedの3通りあり、その内容は次のとおり。
- publicに設定されたメソッドは制限なしに呼び出せる。
- privateに設定されたメソッドは関数形式でしか呼び出せない。
- protectedに設定されたメソッドは、そのメソッドを持つオブジェクトがselfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せる。
publicについては問題ないとしても、privateとprotectedの違いは、上記の説明だけではなかなか分かり難い。そこで、テスト用のプログラムを作り、具体的な動作を確認しながら両者の違いを検証してみた。
呼び出し制限privateの本質
先ずは、privateに設定されたメソッドについて考えるため、次のようなクラスを2つ定義する。ファイル名はsamples_private.rbとし、これをrubyのパスの通っている場所に保存する。
class Sample1 def call_sample1 sample1 # 関数形式での呼び出し end def call_sample1_with_receiver self.sample1 # レシーバー(self)形式での呼び出し end private def sample1 p "只今privateメソッドテスト中!" end end class Sample2 def call_sample1_from_sample2 s1 = Sample1.new s1.call_sample1 # レシーバー形式での呼び出し end def call_sample1_from_sample2_without_receiver s1 = Sample1.new call_sample1 # 関数形式での呼び出し end end
このファイル内のコメントにあるように、関数形式とは、レシーバーを指定しない形式のことをいう。
本来Rubyでは、レシーバーなしでのメソッド呼び出しはできない。このため、自クラス及びサブクラス内のメソッドの呼び出すためには、selfというレシーバーを用いなければならないのだが、例外的にselfレシーバーだけは省略できる。この省略形を関数形式と呼んでいる。
そこで、Sample1クラスでオブジェクトを作り、関数形式とレシーバー形式で、privateに設定されたsample1メソッドを呼び出すプログラムを作って検証してみた。
ファイル名はtest1_private.rbで、samples_private.rbと同じ階層に保存した。
require_relative 'samples_private' s1 = Sample1.new s1.call_sample1 s1.call_sample1_with_receiver
test1_private.rbをターミナル上から実行。
$ ruby test1_private.rb
実行結果は次のとおり。
1行目に、「只今privateメソッドテスト中!」とあるので、privateメソッドであるsample1が関数形式で呼び出され、これが実行されたということが分かる。
これに対して、2行目以降には、レシーバー形式の呼び出しに対するエラーメッセージが表示されている。(NoMethodError)とあることから、レシーバー形式の呼び出しが拒否されたということが分かる。
これにより、リファレンスマニュアルの「privateに設定されたメソッドは関数形式でしか呼び出せない」という原則が守られていることが確認できた。
次に、Sample2クラスからメソッド定義式を経由したメソッドの呼び出しをテストしてみた。実行プログラムの内容は次のとおりで、ファイル名はtest2_private.rbとし、samples_private.rbと同じ階層に保存した。
require_relative 'samples_private' s2 = Sample2.new s2.call_sample1_from_sample2 s2.call_sample1_from_sample2_without_receiver
test2_private.rbをターミナル上から実行。
$ ruby test2_private.rb
実行結果は、次のとおり。
1行目に、”只今privateメソッドテスト中!”とあるので、一見すると、レシーバー形式の呼び出しでprivateに設定された筈のsample1メソッドが実行されているようにも見えるが、s1レシーバーによって呼び出されているのは、あくまでもcall_sample1メソッドである。sample1メソッドそのものは、call_sample1メソッド内から、関数形式で呼び出されているに過ぎない。よって、「privateに設定されたメソッドは関数形式でしか呼び出せない」という原則はこのでも守られていることが分かる。
次に、2行目以降についてだが、こちらもtest1_private.rbのときと同じくエラーメッセージが表示されている。ただ、よく見ると、こちらは、(NameError)とあり、test1_private.rbの実行結果に表示されていた(NoNameErro)とは微妙に違っている。
前述のとおり、関数形式の呼び出しにはselfレシーバーが省略されており、samples_private.rbファイルの28行目は、self.call_sample1と書き換え可能だ。そうなると、これは、Smple2クラス内のcall_sample1メソッド(または変数)を参照するという意味になる。call_sample1メソッドはSmple2クラス内ではなく、Smple1クラス内にあるため、そのようなメソッドはないというエラーメッセージが表示されたというわけだ。
呼び出し制限protectedの本質
次は、protectedに設定されたメソッドについて検証してみる。先ほど作成したsamples_private.rbの11行目から15行目を次のように変更して、rubyのパスの通っている場所に別名保存する。ファイル名はsamples_protected.rbとした。
class Sample1 def call_sample1 sample1 # 関数形式での呼び出し end def call_sample1_with_receiver self.sample1 # レシーバー(self)形式での呼び出し end protected def sample1 p "只今protectedメソッドテスト中!" end end class Sample2 def call_sample1_from_sample2 s1 = Sample1.new s1.call_sample1 # レシーバー形式での呼び出し end def call_sample1_from_sample2_without_receiver s1 = Sample1.new call_sample1 # 関数形式での呼び出し end end
続いて、test1_private.rbの1行目をrequire_relative ‘samples_protected’と変更して、samples_protected.rbと同じ階層に別名保存する。ファイル名はtest1_protected.rbとした。
require_relative 'samples_protected' s1 = Sample1.new s1.call_sample1 s1.call_sample1_with_receiver
test1_protected.rbをターミナル上から実行。
$ ruby test1_protected.rb
実行結果は次のとおり。
これにより、protectedメソッドは、関数形式でもレシーバー形式でも呼び出し可能だということが分かる。
次に、test2_private.rbの1行目をrequire_relative ‘samples_protected’と変更して、samples_protected.rbと同じ階層に別名保存する。ファイル名はtest2_protected.rbとした。
require_relative 'samples_protected' s2 = Sample2.new s2.call_sample1_from_sample2 s2.call_sample1_from_sample2_without_receiver
これをターミナル上から実行する。
$ ruby test2_protected.rb
実行結果は次のとおり。
test2_private.rbの実行結果とほとんど同じで、一行目に”只今protectedメソッドテスト中!”とあり、2行目以降にエラーメッセージが表示されている。エラーメッセージ中に(NameErro)とあるので、その点もtest2_private.rbの場合と同じだ。
これで、「protectedに設定されたメソッドは、そのメソッドを持つオブジェクトがselfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せる」いう原則が、部分的に確認できた。あとは、instance_evalを経由した呼び出しを試せば、完璧だ。
instance_evalメソッドとは、{}で囲まれたブロックをレシーバーのインスタンスの元で実行できるメソッドのことだが、詳しく説明するより、実際に実行プログラムを動かしてみた方が分かりやすいので、次のようなプログラムを新規で作成する。ファイル名はtest3_protected.rbで、同じ階層にsamples_protected.rbがあるものとする。
require_relative 'samples_protected' s1 = Sample1.new s1.instance_eval{sample1} # 関数形式での呼び出し s1.instance_eval{self.sample1} # レシーバー(self)形式での呼び出し
instance_evalメソッドの{}で囲まれたブロックに、インスタンス変数やメソッド定義なども記述できるが、今回のように、レシーバーで指定したクラス内のメソッド名を記述して直接実行することも可能だ。
念のため、関数形式とselfレシーバーを明示した形式の両方で、sample1メソッドを呼び出してみたが、実行結果は、test1_protected.rbと同じく、”只今protectedメソッドテスト中!”というメッセージが2行連続で表示される。
これにより、「protectedに設定されたメソッドは、そのメソッドを持つオブジェクトがselfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せる」という原則が確認できる。
では反対に、「そのメソッドを持つオブジェクトがselfではないコンテキスト」とはどういう文脈のことだろうか。次のプログラムが、その最も簡単な例だ。
require_relative 'samples_protected' s1 = Sample1.new s1.sample1
実際に上記のプログラムをtest_final.rbという名前で保存して、実行すると、エラーメッセージが表示される。エラーメッセージに、(NoMethodError)とあるのを確認しておいていただきたい。
privateとprotectedの根本的な違い
privateとprotectedの違いについての結論を出す前に、privateに設定されたメソッドについて、もうひとうだけテストしておかなければならないことがある。それは、instance_evalメソッドによる呼び出しについてだ。
test3_protected.rbの1行目をrequire_relative ‘samples_private’に変更して、ファイル名をtest3_protected.rbとして、samples_private.rbと同じ階層に別名保存する。
これを実行すると、test1_private.rbの実行結果と同じく、1行目に”只今privateメソッドテスト中!”と表示され、2行目以降にエラーメッセージが表示される。エラーメッセージ中に(NoMethodError)とある。
これで、privateに設定されたメソッドをinstance_evalメソッドで呼び出すことは可能だが、それは関数形式でならなければならないということが分かる。
そして、最後にtest_final.rbの1行目をrequire_relative ‘samples_private’に変更して、これを実行するとエラーメッセージが表示される。エラーメッセージ中に(NoMethodError)とあるのが分かるだろう。
つまり、privateに設定されたメソッドもprotectedに設定されたメソッドも、外部から直接呼びだすことはできないという点ではまったく同じものだと考えてよい。
ただし、呼び出しできない理由が微妙に違うのだ。privateに設定されたメソッドの場合は、関数形式以外(つまりレシーバー形式)の呼び出しという理由から、protectedに設定されたメソッドの場合は、そのメソッドを持つオブジェクトがselfではないコンテキストだという理由から、それぞれ呼び出しが拒否されるのだ。
では、どうやって両者を使い分けたらよいのだろうか。リファレンスマニュアルに次のような例がある。
class Foo def _val @val end protected :_val def op(other) # otherもFooのインスタンスを想定 # _valがprivateだと関数形式でしか呼べないため # このように利用できない self._val + other._val end end
語感的は、privateよりもprotectedの方が制約が多いような気がするが、上記の例に見るように、実際は、privateの方がprotectedより制約が多いのだ。
本来は、外部からの呼び出し制限がprotectedの仕事で、privateはあくまでも関数定義のために作られたものだったが、いつの間には、外部からの呼び出し制限に、protectedよりprivateの方が使われることが多くなったというのが事実のようだ。
そういえば、私も最初からprivate一本槍だ。これはprivateという語感に関係しているのかもしれない。privateだと、このメソッドはあくまでも個人用だよということで、どことなく気軽に使えそうな気がするからだ。
まあ、Ruby on Rails等のRuby製のフレームワークを通じてRubyに触れた人にとっては、privateを使うかprotectedを使うかで悩む場面はほとんどないかもしれないが、ときどきこうやってRubyそのものに触れて、いろいろと疑問を解決してみることも大切かもしれない。