Effective C# Item 34: Avoid Overloading Methods Defined in Base Classes
When a base class chooses the name of a member, it assigns the semantics to that name. Under no circumstances may the derived class use the same name for different purposes. And yet, there are many other reasons why a derived class may want to use the same name. It may want to implement the same semantics in a different way, or with different parameters. Sometimes that's naturally supported by the language: Class designers declare virtual functions so that derived classes can implement semantics differently. Item 33 covered why using the new modifier could lead to hard-to-find bugs in your code. In this item, you'll learn why creating overloads of methods that are defined in a base class leads to similar issues. You should not overload methods declared in a base class.
The rules for overload resolution in the C# language are necessarily complicated. Possible candidate methods might be declared in the target class, any of its base classes, any extension method using the class, and interfaces it implements. Add generic methods and generic extension methods, and it gets very complicated. Throw in optional parameters, and I'm not sure anyone could know exactly what the results will be. Do you really want to add more complexity to this situation? Creating overloads for methods declared in your base class adds more possibilities to the best overload match. That increases the chance of ambiguity. It increases the chance that your interpretation of the spec is different than the compilers, and it will certainly confuse your users. The solution is simple: Pick a different method name. It's your class, and you certainly have enough brilliance to come up with a different name for a method, especially if the alternative is confusion for everyone using your types.
The guidance here is straightforward, and yet people always question if it really should be so strict. Maybe that's because overloading sounds very much like overriding. Overriding virtual methods is such a core principle of object-oriented languages; that's obviously not what I mean. Overloading means creating multiple methods with the same name and different parameter lists. Does overloading base class methods really have that much of an effect on overload resolution? Let's look at the different ways where overloading methods in the base class can cause issues.
There are a lot of permutations to this problem. Let's start simple. The interplay between overloads in base classes has a lot to do with base and derived classes used for parameters. For all the following examples, any class that begins with "B" is the base class, and any class that begins with "D" is the derived class. The samples use this class hierarchy for parameters:
public class B2
{ }public class D2
:B2
{}
Here's a class with one method, using the derived parameter (D2):
public class B
{public void
Foo(D2
parm) {Console
.WriteLine("In B.Foo"
); } }
Obviously, this snippet of code writes "In B.Foo":
var
obj1 =new D
(); obj1.Bar(new D2
());
Now, let's add a new derived class with an overloaded method:
public class D
:B
{public void
Foo(B2
parm) {Console
.WriteLine("In D.Foo"
); } }
Now, what happens when you execute this code?
var
obj2 =new D
(); obj2.Foo(new D2
()); obj2.Foo(new B2
());
Both lines print "in D.Foo". You always call the method in the derived class. Any number of developers would figure that the first call would print "in B.Foo". However, even the simple overload rules can be surprising. The reason both calls resolve to D.Foo is that when there is a candidate method in the most derived compile-time type, that method is the better method. That's still true when there is even a better match in a base class. Of course, this is very fragile. What do you suppose this does:
B
obj3 =new D
(); obj3.Foo(new D2
());
I chose the words above very carefully because obj3 has the compile-time type of B (your Base class), even though the runtime type is D (your Derived class). Foo isn't virtual; therefore, obj3.Foo() must resolve to B.Foo.
If your poor users actually want to get the resolution rules they might expect, they need to use casts:
var
obj4 =new D
();((
.Foo(B
)obj4)new D2
()); obj4.Foo(new B2
());
If your API forces this kind of construct on your users, you've failed. You can easily add a bit more confusion. Add one method to your base class, B:
public class B
{public void
Foo(D2
parm) {Console
.WriteLine("In B.Foo"
); }public void
Bar(B2
parm) {Console
.WriteLine("In B.Bar"
); } }
Clearly, the following code prints "In B.Bar":
var
obj1 =new D
(); obj1.Bar(new D2
());
Now, add a different overload, and include an optional parameter:
public class D
:B
{public void
Foo(B2
parm) {Console
.WriteLine("In D.Foo"
); }public void
Bar(B2
parm1,B2
parm2 =null
) {Console
.WriteLine("In D.Bar"
); } }
Hopefully, you've already seen what will happen here. This same snippet of code now prints "In D.Bar" (you're calling your derived class again):
var
obj1 =new D
(); obj1.Bar(new D2
());
The only way to get at the method in the base class (again) is to provide a cast in the calling code.
These examples show the kinds of problems you can get into with one parameter method. The issues become more and more confusing as you add parameters based on generics. Suppose you add this method:
public class B
{public void
Foo(D2
parm) {Console
.WriteLine("In B.Foo"
); }public void
Bar(B2
parm) {Console
.WriteLine("In B.Bar"
); }public void
Foo2(IEnumerable
<D2
> parm) {Console
.WriteLine("In B.Foo2"
); } }
Then, provide a different overload in the derived class:
public class D
:B
{public void
Foo(B2
parm) {Console
.WriteLine("In D.Foo"
); }public void
Bar(B2
parm1,B2
parm2 =null
) {Console
.WriteLine("In D.Bar"
); }public void
Foo2(IEnumerable
<B2
> parm) {Console
.WriteLine("In D.Foo2"
); } }
Call Foo2 in a manner similar to before:
var
sequence =new List
<D2
> {new D2
(),new D2
() };var
obj2 =new D
(); obj2.Foo2(sequence);
What do you suppose gets printed this time? If you've been paying attention, you'd figure that "In D.Foo2" gets printed. That answer gets you partial credit. That is what happens in C# 4.0. Starting in C# 4.0, generic interfaces support covariance and contravariance, which means D.Foo2 is a candidate method for an IEnumerable<D2> when its formal parameter type is an IEnumerable<B2>. However, earlier versions of C# do not support generic variance. Generic parameters are invariant. In those versions, D.Foo2 is not a candidate method when the parameter is an IEnumerable<D2>. The only candidate method is B.Foo2, which is the correct answer in those versions.
The code samples above showed that you sometimes need casts to help the compiler pick the method you want in many complicated situations. In the real world, you'll undoubtedly run into situations where you need to use casts because class hierarchies, implemented interfaces, and extension methods have conspired to make the method you want, not the method the compiler picks as the "best" method. But the fact that real-world situations are occasionally ugly does not mean you should add to the problem by creating more overloads yourself.
Now you can amaze your friends at programmer cocktail parties with a more in-depth knowledge of overload resolution in C#. It can be useful information to have, and the more you know about your chosen language the better you'll be as a developer. But don't expect your users to have the same level of knowledge. More importantly, don't rely on everyone having that kind of detailed knowledge of how overload resolution works to be able to use your API. Instead, don't overload methods declared in a base class. It doesn't provide any value, and it will only lead to confusion among your users.