Virtual Methods vs Extension Methods

I want to follow up my last post with a clear example showing what I expected to happen and why it didn't.  What is the output of the following program?

    class Program

    {

        static void Main(string[] args)

        {

            Bar bar = new Bar();

            Foo foo = bar;

 

            foo.Say();

            bar.Say();

        }

    }

 

    class Foo

    {

        public virtual void Say() { Console.Write("Foo"); }

    }

 

    class Bar : Foo

    {

        public override void Say() { Console.Write("Bar"); }

    }

Anyone who has a decent grasp of inheritance and polymorphism should have correctly guessed that the output of the program is BarBar.  The class Bar overrides the default behaviour of Foo and replaces it with something else.  What if we use extension methods?

    class Foo {}

    class Bar : Foo {}

 

    static class Extensions

    {

        public static void Say(this Foo foo) { Console.Write("Foo"); }

        public static void Say(this Bar bar) { Console.Write("Bar"); }

    }

Because extension methods are a new addition to the framework people are more likely to get this wrong, like me.  The output of this program is FooBar.  The important difference is that the extension method on Bar is not able to override and replace the default behaviour on Foo and is only used if you refer to the object as a Foo.

If you're designing an API and want to avoid confusion, watch out for this one.  As always there's workarounds, here's something you might try:

    static class Extensions

    {

        public static void Say(this Foo foo)

        {

            if(foo is Bar)

                ((Bar)foo).Say();

            else

                Console.Write("Foo");

        }

 

        public static void Say(this Bar bar) { Console.Write("Bar"); }

    }

This produces the same result as the original virtual method based program, BarBar.

While this might make you API a little more predictable it does so at the expense of the code.  Tying Foo to it's derived classes like this starts to break down the reasons for using the Foo abstraction in the first place and will make your code difficult to modify in future.  I would suggest these issues as the reason Microsoft didn't add a special case for IQueryable<T> in IEnumerable<T>.Where, but it turns out they have already added special cases for arrays and lists so who knows.

It's the little details like this that make great interview questions for those annoying candidates that seem to know everything...

22/01/2009 04:50 PM (UTC -08:00)
Comments are closed.