Overloaded Functions

Quality
2021-01-12

An often under used feature of C# is overloaded functions.

Overloaded Truck

Let's jump into the code.

//v1
public void Notify(IEnumerable recipients, string message)
{
  if (!recipients.Any())
  {
    throw new InvalidOperationException();
  }

  foreach (var person in recipients)
  {
    SendMessage(person, message);
  }
}

It is a nice, simply little function. However, there is a small potential performance problem with it. The recipients IEnumerable parameter may be evaluated multiple times (once by the call to Any, and again in the foreach loop). This could be a problem, especially if the LINQ provider for the IEnumerable is a ORM database query.

The solution is to ensure the IEnumerable is a converted to a List (assuming it is not already a List).

//v2
public void Notify(IEnumerable recipients, string message)
{
  var recipientsList = recipients as IList 
    ?? recipients.ToList();

  if (!recipientsList.Any())
  {
    throw new InvalidOperationException();
  }

  foreach (var person in recipientsList)
  {
    SendMessage(person, message);
  }
}

Although this function has better performance, there is something that is problematic. The number of variables being managed by the function is growing. Specifically, there is now a recipients parameter and a recipientsList variable. The two variables are remarkably similar, which leads to a couple issues.

The first issue is that we have trouble coming up with good names for these variables. Are not they both recipients? I would really like to call them both recipients, but of course the compiler won’t let me. Some work arounds for this would be use Hungarian Notation on the variables (yuck). In this case I suffixed the list with List and left the IEnumerable parameter unchanged. I choose to name the parameter as recipients because that is what is exposed to the callers of this public function. It is viewed more often from Intellisense, therefore it deserves the better name.

The second issue is that it is easy to use the wrong variable within the function. Both variables are in scope for the entire length of the function. If I were doing maintenance on this code and needed to type recipientsList to pass it to a new function I was calling it would be extremely easy to accidentally type the recipients parameter name by mistake. Ideally, we want to make sure that the list variable is the only recipients variable in scope.

The solution to both these issues: overload the function.

//v3
public void Notify(IEnumerable recipients, string message) => 
  Notify(recipients as IList ?? recipients.ToList(), message);

private void Notify(IList recipients, string message)
{
  if (!recipients.Any())
  {
    throw new InvalidOperationException();
  }
  foreach (var person in recipients)
  {
    SendMessage(person, message);
  }
}

Here the first overload takes care of ensuring we are working a list. The second overload is cleaner because its only responsibility is sending messages and not dealing with the Type System. Note that there is only ever one parameter in scope at a time that represents the recipients. This makes it easy to name and hard to misuse.

This is one example of where I often use overloads when refactoring code. Another common example would be to taking Nullable types, dealing with those nulls (either by throwing exceptions or applying default values), then call an overload where the compiler will not allow nulls. There are many other cases where overloads can be beneficial to a code base.

Before I close out this post, let me add a couple tips for working with overloads. Do follow Postel’s Law of being “liberal in what you accept” as parameters. That doesn’t mean you have to take a System.Object as parameters and add a lot of type checking code to your functions. Let the compiler deal with as many typing issues as possible (that’s what it is there for). Using overloads is a good way to let the compiler worry about typing and keeping your code clean.

Also, don’t let overloads be the source of code duplication. Overloaded functions should almost always be calling each other. The overloads take care of dealing with the various parameters, and the base method that is called has the single source of truth of the responsibility and implementation of the function.