サブクラスから自身がオーバーライドしたスーパークラスのメソッドを呼びたいとき 2


id:sumim:20061201:p1 の続き。ここで、Ruby でメソッドを“引っこ抜いて叩く”例に相当するものを SqueakSmalltalk で次のように書いたところなのですが…

D >> bar2
  | method |
  method := self class superclass lookupSelector: #foo.
   ^ method valueWithReceiver: self arguments: #()


これですと、D のサブクラス DD で #bar をコールしたとき、期待通りの動作をしません。

DD new bar2   " => 'D >> #foo' "


Ruby の例でも同様です。

class DD2 < D2; end
DD2.new.bar   #=> "D#foo"


この問題は、self class(Ruby では self.class)から superclass でスーパークラスを動的にたぐっているために起こります。本当に知りたいのは「self の属するクラスのスーパークラス」ではなく、「実行中のメソッド(D >> #bar2 、D2#bar)を定義しているクラスのスーパークラス」なので、両者がたまたま一致した場合にしか、期待通りの動作をしなかった…というわけです。


ところで、Ruby において B#foo を super_foo として alias するパターンでは、同様の問題は起こりません。なぜでしょう。

Ruby の alias は、alias 時の旧名で参照できるメソッドを指定した別名でコールできるようにする機能を持つので、実行時の文脈における self に関係なく super_foo はあいかわらず、B#foo を指し続け、それをコールするのに使えるからです。

class DD1 < D1; end
DD1.new.bar   #=> "B#foo"


これをヒントに、メソッドを“引っこ抜いて叩く”例でも、コールするメソッドを B#foo としてハードコードしてしまえば問題は起こらないことが分かります。

class D2
  def bar2
    B.instance_method(:foo).bind(self).call
  end
end
D2.new.bar2    #=> "B#foo"
DD2.new.bar2   #=> "B#foo"


…というより、なんの気なしにかっこいいから…(^_^;)と、 self.class.superclass と記述してしまいましたが、これはある種の(継承したクラスからコールされることを想定できなかった設計上の…)バグですね。もちろん、冒頭の Smalltalk の例でもまったく同じです。

D >> bar4
  | method |
  method := B lookupSelector: #foo.
   ^ method valueWithReceiver: self arguments: #()
D new bar4    " => 'B >> #foo' "
DD new bar4   " => 'B >> #foo' "


ところで Smalltalk では擬変数「thisContext」でその時点でのコンテキスト(実行中のメソッドの環境、あるいは、インタープリタの内部状態…ともいえる情報)にアクセスでき、さらにそこから(当然のように)実行中のメソッドをたぐることが可能です。これを使えば、あくまで動的な記述にこだわりつつ、しかし期待通りの動作をしてくれるような記述もできます。

D >> bar4
  | superclass method |
  superclass := thisContext methodClass superclass.
  method := superclass lookupSelector: #foo.
  ^ method valueWithReceiver: self arguments: #()
D new bar4    " => 'B >> #foo' "
DD new bar4   " => 'B >> #foo' "


念のためくどいようですが、そもそも最初から super foo と表現しておけば一連の問題はいっさい気にする必要はありません。

D >> bar
   ^ super foo
D new bar    " => 'B >> #foo ' "
DD new bar   " => 'B >> #foo ' "