The process by which the compiler decides which method overload to use for a particular call is called overload resolution. The C# language specification (ECMA-334) defines the rules used for this process in section 14.4.2.

Consider the following class hierarchy:

Code:

public class Base {
   public void Print( string text ) {
      Console.WriteLine("string: {0}", text);
   }

   public void Print( int num ) {
      Console.WriteLine("int: {0}", num);
   }
}

public class Derived : Base {
   public void Print( object obj ) {
      Console.WriteLine("object: {0}", obj);
   }
}

This *bad* design defines a method in the derived class that has a parameter that is less specific than those in the base class. This is almost always a bad idea, and here’s an example of why:

Code:

using System;

namespace OverloadExplorer {
   internal class Program {
      private static void Main() {
         Derived x = new Derived();

         string s = "one";
         int n = 1;

         x.Print(s);
         x.Print(n);

         Console.ReadLine();
      }
   }
}

Output:

object: one
object: 1

Both calls to Print() execute in the derived class, completely ignoring the more specific “overloads” in the base. Let’s consider a few ways to prevent the compiler from making this tolerated mistake (the real culprit is, after all, our own *bad* class design), and perhaps at the same time we’ll get to understand exactly why this is happening.

A couple of obvious possible changes in the caller jump up at me… Consider, for instance, casting ‘x’ to Base on each call to Print():

Code:

((Base)x).Print(s);
((Base)x).Print(n);

Or, even simpler, declaring ‘x’ as Base at the outset:

Code:

Base x = new Derived();

In both these cases, we get the expected output:

Output:

string: one
int: 1

However, these options do nothing to improve our design, and there are a couple of additional (and somewhat more interesting) ways to accomplish what we want:

1. Change all the “overloads” to use ref parameters

Code:

using System;

namespace OverloadExplorer {
   internal class Program {
      private static void Main() {
         Derived x = new Derived();

         string s = "one";
         int n = 1;

         x.Print(ref s);
         x.Print(ref n);

         Console.ReadLine();
      }
   }

   public class Base {
      public void Print( ref string text ) {
         Console.WriteLine("string: {0}", text);
      }

      public void Print( ref int num ) {
         Console.WriteLine("int: {0}", num);
      }
   }

   public class Derived : Base {
      public void Print( ref object obj ) {
         Console.WriteLine("object: {0}", obj);
      }
   }
}

This one in particular is quite an interesting find. It’s kind of intuitive and I couldn’t quite explain it at first, but revisiting section 14.4.2.1 of the spec offered up the following:

[when using an argument list to determine the list of applicable function members]

  • for a value parameter or a parameter array, an implicit conversion (§13.1) exists from the type of the argument to the type of the corresponding parameter, or
  • for a ref or out parameter, the type of the argument is identical to the type of the corresponding parameter. [Note: After all, a ref or out parameter is an alias for the argument passed. end note]

Thus, we are in effect forcing the compiler to find an exact type match for the ref parameters, and we get the desired output as below.

Output:

string: one
int: 1

Of course, again this does nothing to improve the design.

2. Fix the design

Okay, so we’ve said a couple of times that the design is *bad* (check that, three times) – but what exactly is wrong with it?

Here’s the rub: if you’re trying to introduce an “overload” member in a type that is separate from the type containing the “overloaded” member, then it is not strictly speaking an overload – it’s just another member.

We find a clue to this in section 10.6 of the spec: Overloading of methods permits a class, struct, or interface to declare multiple methods with the same name, provided their signatures are unique within that class, struct, or interface.

That is, overloads are defined in a single type (notice how up until now I’d been referring to our “overloads” using quotes).

Consider the following (more explicit) example of what we did wrong:

Code:

public class Base {
   public void Print( string text ) {
      Console.WriteLine("string: {0}", text);
   }

   public void Print( int num ) {
      Console.WriteLine("int: {0}", num);
   }

   public void Print( object obj ) {
      Console.WriteLine("base object: {0}", obj);
   }
}

public class Derived : Base {
   public void Print( object obj ) {
      Console.WriteLine("object: {0}", obj);
   }
}

This explicitly shows that we are not in fact adding an overload, but instead we’re hiding the base implementation of Print(object). Note that this is semantically and behaviorally identical to our original example.

Output:

object: one
object: 1

At this point our design is still quite bad, though. For starters, we are implicitly hiding a member from the base class, and should do so explicitly using the new keyword instead.

Code:

public new void Print( object obj ) {
   Console.WriteLine("object: {0}", obj);
}

Although we’re still spewing the unwanted output, we’ve now improved our design to simply bad. On the other hand, we’ve also pointed out (by being explicit) exactly what’s wrong with it. We’ve been modifying our base functionality, instead of extending it. [see Open-Closed Principle]

Here then, is what we should have been doing in the first place:

Code:

public class Base {
   public void Print( string text ) {
      Console.WriteLine("string: {0}", text);
   }

   public void Print( int num ) {
      Console.WriteLine("int: {0}", num);
   }

   public virtual void Print( object obj ) {
      Console.WriteLine("base object: {0}", obj);
   }
}

public class Derived : Base {
   public override void Print( object obj ) {
      Console.WriteLine("object: {0}", obj);
   }
}

And F5 reveals we’re sorted. Print(object) is now an *actual* overload in the base class, and the compiler is freed to correctly apply the overload resolution rules set out in the specification, giving us our desired output.

Output:

string: one
int: 1