ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【Ruby v2.6】protected とクラスメソッド、インスタンスメソッドの関係 ※2020.8.25 追記

前置き

C#JAVA でのメソッドの protected 指定は、「そのクラスと継承したクラスからアクセス可能」という意味ですが、Ruby の場合はちょっと挙動が異なるみたいなので、まとめてみました。

protected なクラスメソッド、protectedインスタンスメソッドの関係

結論としては、以下の2点です。

  • protected なクラスメソッドは、インスタンスメソッドからは、同じクラスからも継承関係にあるクラスからも呼び出せない。
  • protectedインスタンスメソッドは、クラスメソッドからは、同じクラスからも継承関係にあるクラスからも呼び出せない。

C#JAVA の場合、クラスメソッド、インスタンスメソッドの括りなんか関係なく、同じクラス、継承関係にあるクラスのメソッドから呼び出せるのですが、Ruby では、インスタンスメソッド、クラスメソッドの間では protected はアクセスできないのです。

例えば・・・

ぱっと思いつく例としては、

  • クラス A のクラスメソッドから、各インスタンスインスタンスメソッドにアクセスして、持っている値を収集したい。でも別の関係ないクラスから値を参照されても困るから、インスタンスメソッドを protected にしておこう。。。

とか、逆に・・・

  • クラス A の各インスタンスから、処理した内容をクラス A の変数に集約するために、クラス A に情報集約用のクラスメソッドを作ろう。でも、クラス A のインスタンス以外からアクセスされたら困るから、クラスメソッドを protected にしておこう。。。

ということができないのです。この場合、public のままで妥協するか、別の手段を考える必要があります。

なんでこんな仕様なのか、自分なりに考えてみた。

最初は、すごく不思議な仕様な感じがしましたが、インスタンスメソッド、クラスメソッドの格納先を分けて考えてみると、少しだけ「なるほど」と思えるようになりました。

Ruby では、インスタンスメソッド、クラスメソッドの定義は同じ場所に書きますが、実体としては、インスタンスメソッドはクラスに、クラスメソッドは特異クラスに格納されます。ですので、あえて図でそのように分けて書いてみると、下記のようになります。

f:id:gyobo:20200824215801p:plain
protected メソッドの呼び出し可否

この図で、左が「クラス A」、右が「クラス A の特異クラス」です。「クラス A」で定義しているのがインスタンスメソッド、「クラス A の特異クラス」で定義しているのがクラスメソッドになります。

  • クラス A のインスタンスメソッドpublic_ins_method から、protected なインスタンスメソッドはアクセス OK、protected なクラスメソッドはアクセス NG です。
  • クラス A のクラスメソッドpublic_class_method から、protected なインスタンスメソッドはアクセス NG、protected なクラスメソッドはアクセス OK です。

図で見ると一目瞭然ですが、同じクラス内に格納されていればアクセスOK、そうでなければアクセス NG ということが分かります。

さらに、クラス A を継承したクラス B をつけてみます。

f:id:gyobo:20200824221118p:plain
継承したクラスからのアクセス可否

クラス A とクラス B 、クラス A の特異クラスとクラス B の特異クラス は、それぞれ継承関係にあります。 継承関係にあるクラス間であればアクセス OK ですが、やはりタスキがけのアクセスは NG となっています。

このことから。

  • protected は、同じクラス、もしくは継承関係にあるクラスのメソッドからアクセスが可能、というのは正しい。
  • ただ、Ruby の場合は、インスタンスメソッドとクラスメソッドの格納先が異なっている。
  • インスタンスメソッドとクラスメソッドの間では「実行しているクラスが異なり」、「クラスと特異クラスは継承関係にない」ので、protected ではアクセス NG となる。

ということなのかなー、と勝手に理解してみました。

※ あえてかぎかっこ「」で括った文言は、深くツッコみだすとはまりそうな気がするので、これ以上はツッコまないことにします。

追記(2020.8.25)

「継承関係にないとアクセスできない」というところで、ふと、思い付きがありました。

以前の記事で書いたのですが、特異クラスの親を追っていくと、BasicObjectの特異クラスから Classクラスに回って、最終的にBasicObject にたどりきます。 図で書いたのを再掲しますと。

f:id:gyobo:20200802223927p:plain
クラス階層

クラス A を普通に生成した場合、Objectクラスからの派生になるので、クラス A の特異クラスの継承ルートにクラス A は入ってきません。

じゃあ、Objectクラスだったら?Object クラスの特異クラスの場合、継承ルートにObject クラスが入ってきます。

ということは、Objectクラスのクラスメソッドだったら、Objectクラスの protected インスタンスメソッドにアクセスできる?

というわけで、試してみました。 比較のために、Object クラス と クラス A に、インスタンス生成のところだけを変えた同じメソッドを追加してみました。

class Object
  def self.class_method_test
    obj = Object.new                    # <= 違いはここだけ
    obj.protected_method_test
  end
  
  protected
  
  def protected_method_test
    "called protected_method_test"
  end
end

class A
  def self.class_method_test
    obj = A.new                      # <= 違いはここだけ
    obj.protected_method_test
  end
  
  protected
  
  def protected_method_test
    "called protected_method_test"
  end
end

結果は、こうなりました。

A.class_method_test        # => NoMethodError (protected method `protected_method_test' called for #<A:0x0000000005812310>)
Object.class_method_test   # =>  "called protected_method_test"

Object クラスの方は、protected なインスタンスメソッドにアクセスできました。やっぱり、継承関係か否か、で見てるっぽいですね。なので、Object クラスやClassクラス、Moduleクラスの Protected インスタンスメソッドなら、同じクラスのクラスメソッドからアクセス可能です。

・ ・ ・

でもこんなことわかっても、役に立ちそうなシーンが思い浮かばないですね・・・ そこまでして protected を使う意味がなさそうです。