Freigeben über


All your base do not belong to you

People sometimes ask me why you can’t do this in C#:

class GrandBase
{
public virtual void M() { Console.WriteLine("GB"); }
}

class Base : GrandBase
{
public override void M() { Console.WriteLine("B"); }
}

class Derived : Base
{
public override void M()
{
Console.WriteLine("D");
base.base.M(); // illegal!
}
}

The author of the most-derived class here wishes to call its GrandBase implementation of M, rather than the Base implementation of M. It wishes to do an "end-run" around its base class.

This is not legal in C#, and is a bad programming practice. If you find yourself in a position where you want to do an end-run around your base class, odds are good that there is a larger flaw in your type hierarchy that needs to be fixed.

The fact that Base is derived from GrandBase is a “public” part of the surface area of Base. But the fact that Base.M overrides GrandBase.M is not part of that public surface area; by inheriting from GrandBase, Base is promising to provide an implementation of M, but whether that implementation is an override, or merely defers directly to the GrandBase implementation is an implementation detail of Base. The Derived class should not know or care how Base fulfills its contract; merely that it does so satisfactorily.

Moreover, when the author of Derived derived it from Base, presumably that developer did so because they liked Base and wanted to re-use its implementation details. If you don’t like the implementation details of Base then don’t derive from it in the first place; derive from GrandBase directly.

This is also a bad idea and therefore illegal because it is fragile. Suppose we have a slightly more complex scenario. Let’s add a protected virtual method P to GrandBase. Suppose GrandBase.M calls P, and suppose Base overrides P. If Base.M sets up some state that Base.P depends on, then Derived doing an end-run around Base.M means that GrandBase.M will call Base.P without Base.M setting up the state it needs.

class GrandBase
{
public virtual void M() { Console.WriteLine("GB"); P(); }
protected virtual void P() { }
}

class Base : GrandBase
{
public override void M()
{
Console.WriteLine("B");
// We know there is about to be a call to P.
SetUpStateForP();
}

protected override void P()
{
// Use the state set up by M
...

This could have an impact on security or correctness. It is hard enough to design correct, secure, robust implementations of virtual methods; let’s not make it any harder.

Now, I note that this is only a rule of C#, not a rule of the CLR. The CLR does allow a language to implement a feature whereby a non-virtual call is done to a virtual method that skips arbitrarily far down the class hierarchy. You cannot rely upon the CLR enforcing this rule of C#. However, the CLR does not allow non-virtual calls from a non-derived type. If you made another type, C, that was not derived from GrandBase then a non-virtual call to GrandBase.M would not be verifiable. Interestingly enough, this rule applies even to nested types; the CLR verifier does not allow a nested type to do a non-virtual call to a virtual method of its container's base class.

Comments

  • Anonymous
    December 12, 2010
    If you had to do it again, would you make nested types' non-virtual calls to virtual methods of their container's base verifiable? The upside is that it would make the use case of calling "base" methods from closures verifiable. What would be the downside?

  • Anonymous
    December 13, 2010
    Great article. I know I tried to do something like that before, and it felt wrong, but I couldn't put my finger on exactly what was wrong with it... I love the Zero Wing reference in the title, but I think there's a mistake, it should be "All your base ARE not belong to you" ;)

  • Anonymous
    December 13, 2010
    I've seen this kind of issue arise quite often in UI frameworks that rely on inheritance to compose functionality. So, for instance, you may have some base class Control (for instance) that provides virtual methods for doing things like layout rendering visual chrome. You then have directed classes that extend Control and provide custom behavior by overriding these methods. So let's say there's a TextBox class inheriting from Control that overrides Layout( ) and performs it's own custom logic, and then delegates back to Control to allow some common layout logic to be applied as well. Developers who inherit from TextBox and who desire most of the functionality that TextBox adds but want to override layout processing are in a bit of a bind. They can override Layout( ) but they can't call down into Control to invoke the common layout processing without calling through TextBox's implementation of Layout( ) ... which is exactly what they don't want. It's cases like this that are hard to support when inheritance is used to propagate common behavior to a set of related classes.

  • Anonymous
    December 13, 2010
    "odds are good that there is a larger flaw in your type hierarchy that needs to be fixed." "your" is important. Situations like this arise when GrandBase and Base are not "your" classes. And you want your class (Derived) to do something very similar to Base, but a little bit different. Reimplementing all of Base's logic is often not a good solution either.

  • Anonymous
    December 13, 2010
    "But the fact that Base.M overrides GrandBase.M is not part of that public surface area" Unfortunately this is only true if you're just interested in source compatibility, not binary compatibility. Can't find your blog post on it. Interestingly from what I remember you argued that removing/adding an override is an obvious breaking change. And I find it a bit annoying that calling base.base.M() from another language is possible/creates verifiable code. It might cause people not aware of this to create insecure code where they believe that grandbase.M() is secured from the grandchildren because it was overriding in base.M(). Or as a similar discrepancy the this!=null rule from C#. But I guess that's the price we have to pay for having a multi language runtime, since languages like C++ allow calling such methods.

  • Anonymous
    December 14, 2010
    Great post. I know a few people who asked that same question, now I can just point them to your post. Just one little detail you might want to fix: in the last example, in Base.M, you might want to add a call to base.M() after SetUpStateForP(), so that P() would get executed in your example code. It's not necessary to get the point across, but it stops people from wondering "wait, why are we setting up state for P, if P is not really getting called".

  • Anonymous
    December 14, 2010
    Excellent post, thanks

  • Anonymous
    December 14, 2010
    The comment has been removed

  • Anonymous
    December 14, 2010
    The comment has been removed