第7条:了解super的不同行为
假设你已经写好了一个类,这个类继承自一个基类。而被继承的类定义了一个不那么适合新类的方法。因此你决定改进那个方法,但是你不想完全替代既有方法,因为它承担了90%的必要且繁重工作。你也不想改变基类,因为那将破坏其他代码的功能。先忽略掉使用组合而非继承的这个更好的选项,让我们费点力气来重载这个方法。你最后做了这些改动:
你让自己陷入了困境。最大的问题是:你如何调用超类中的m1方法?如果你试图调用衍生类中的方法,你将陷入无限循环中。那没什么帮助。这就到了super出场的时候了:
现在我们有些眉目了。新版的m1方法使用super来调用父类的m1方法。本例中,super有点替代Base#m1的意思。你在能够直接使用Base#m1的任何地方都能使用super。这包括通过super传递你希望传递给Base类的m1的任何参数。(当然仅限于目标方法能够接收的那些参数。)小心这个陷阱。
即使super被用作并扮演得像个方法,它其实是Ruby的关键字。这之间的不同很重要,因为super是基于Ruby中设定为可选的元素——括号,来改变行为的。我说得很严肃哦,请仔细想上一秒。当使用super时,如果省略了括号是会改变其行为的。这比听上去还要严重。要知道括号改变了什么,我们需要回顾一下super的三种写法:
super的第一种用法最为平常——就是我们之前所见的那样。如果你至少给super一个参数,那么它的行为就好像常规的Ruby方法,并且括号是可选的。通过这种形式,super将参数全部传递给目标方法。
如果没有给super任何参数和括号,它的行为可能会和你的预期不同。这种用法之下,super在调用目标方法时将宿主方法(enclosing method)的所有参数全部传递过去。如果存在方法块它也会一并传递。
你应该可以看出,这种形式下的super会带来一些副作用。首先,这样使用super仅仅在目标方法和宿主方法接收相同数量的参数时才可用。比如,这对我们之前看到的例子中的方法Base#m1和Derived#m1就不适用。试图在Derived#m1方法中使用无参的super会引发一个ArgumentError异常。如果被重载的方法不接收参数而宿主方法接收,那么着同样会为你带来一个相同的异常。
可以看出的第二件事与传递给重载方法的参数的值有关。如果你在使用无参形式调用super之前,在宿主方法中改变了其参数中的一个值,那么super会将改变后的当前值传递给目标方法,而非原始的值。这看起来非常合理,但有时你也需要注意。
当你希望在使用super时不将任何参数传递给重载方法,你需要使用空括号,即super()。在Ruby中这种形式看起来实在很怪。即使我们喜欢使用括号,在无参调用时也不会加上它们。这种super的用法看起来有些不自然,但这是无参(也没有块)调用重载方法的唯一途径。
以下是对上述规则的代码表述:
下一个需要关心的是super如何寻找要重载的方法。你也许会简单地想,只要调用其超类的同一方法就可以了,但这也简单地过分了哦。事实上,super是从整个继承体系中寻找的,见第6条,不过有些许微小的差异。使用super时,它从继承体系的上一层去寻找和当前方法同名的方法。所以,它将从当前类的上一层开始寻找。
如果你还记得我们对继承体系的研究,你将明白上一层可能会是一个单例类。这很棒啊,因为这意味着super能够对其引用的模块中的方法进行重载。
这个例子也指出了使用super的一个限制。如果你希望调用一个定义在超类中的方法,而与此同时如果包含的模块中也定义了同名方法,super就无法帮你达到目的。显然,super会在找到第一个匹配的同名方法后停下来,而那是包含的模块中的方法,而不是超类中的。但如果你真的意外遇到了这种情况,那么可能在设计上存在严重的问题。可以考虑使用组合而非继承了。
最后一个需要警示的是super是如何与method_missing相互作用的。如果你思考过这个问题,你会知道,只要在继承体系中存在另一个同名方法,使用super就不会引发method_missing。另一方面,如果你恰巧是一个瞌睡的开发者或者正在使用一些元编程的黑魔法,你最终可能会在使用super时引发method_missing。如果是这样,你读到这节会非常开心。
就像一个普通的方法查找一样,如果super无法找到它要寻找的方法,将调用method_missing来报告这个错误。如你料想的那样,Ruby寻找method_missing时会从原接收者所在的类开始查找。这一切都很好,我们还可以从异常结果中得到有用的错误信息。
Ruby会在内部追踪method_missing方法是否由一个错误的super调用引发,若是如此,会在异常中给出一些额外的信息。但这仅发生在Ruby没能在继承体系中找到已定义的method_missing方法而使用默认实现时。一旦继承体系中存在任何定义了method_missing方法的类,你就会丢失这个有用的细节。这个信息无法找回,即使你在自己的method_missing实现中调用super也不行。
再多说点,method_missing在被调用时描述的细节是说,方法是存在的,只是它在错误的地方。如果上面的SuperHappy类定义了method_missing方法,它将在调用laugh时被调用,因为那个方法试图使用super调用另一个不存在的laugh方法。那么method_missing的第一个参数是什么呢?对的,就是缺少的方法的名字,本例中就是:laugh。但是SuperHappy本身是有laugh方法的。有点迷茫了是不是?
假如你想细数避免自行定义method_missing方法的原因,请确保你也算上了这个(包括第30条里提到的)。本例中,来自method_missing方法的默认实现中的NoMethodError异常显然比我们自己定义的method_missing方法更好。
要点回顾
当你想重载继承体系中的一个方法时,关键字super可以帮你调用它。
不加括号地无参调用super等价于将宿主方法的所有参数传递给要调用的方法。
如果希望使用super并且不向重载方法传递任何参数,必须使用空括号,即super()。
当super调用失败时,自定义的method_missing方法将丢弃一些有用的信息。在第30条中有method_missing的替代解决方案。