09 September 2010

Optional Argument Overload Bear Traps

C# programmers, beware the optional argument overload trap!  If you don't already know about Optional Arguments and Named Arguments, read through my post, Introduction to Optional Arguments and Named Arguments.

Unfortunately, one of my clients is not a C++ programmer, and encountered this issue, when updating a common library.  Realizing this issue may severely impact large applications (i.e. enterprise, B2B, etc.), I thought it best to discuss it openly.  Using optional arguments may be setting yourself a trap that will be difficult to escape, in the future.


Incidentally, Phil Haack has posted this issue in greated detail, in his Versioning Issues With Optional Arguments and More Versioning Fun With Optional Arguments posts.  I recommend also reading his posts.

About the Compiler

To understand the issue, we must first know how the .NET compiler handles optional arguments.

Optional arguments are a means of eliminating or reducing the need to write method overloads.  The C# compiler determines what overloads are actually needed, by analyzing references to the method, and then creating an overload for each parameter combination actually called from other classes.

So, what happens if we define these method overloads, and then try the following call?

string Greeting(
    string greeting            // Required argument
    ,string subject = "world") // Optional argument
{
  return string.Format("{0}, {1}!", greeting, subject);
}

string Greeting(
    string greeting = "Hello"  // Optional argument
    ,string subject = "world") // Optional argument
{
  return string.Format("{0}, {1}!", greeting, subject);
}

void CallGreeting()
{
    Greeting("Holy ambiguity", "Batman");
}

The compiler can't tell which class is supposed to be called.  Fortunately, Visual Studio reports an ambiguity issue, and the compiler produces an error:
Type 'DemoProgram.Program' already defines a member called 'Greeting' with the same parameter types
Problem solved, right?  Well...


The Overload Trap

The real issue comes when we have existing code that we then modify, not to include an overload with more paramteres, but to create on with fewer parameters of matching types, which also include optional arguments.  (Oh, no!)

Most developers will immediately red card the previous statement, and suspend it from the game -- it's the football ("soccer") equivalent of intentionally cleating the opposing team's coach, and then the referee.  (If you have no idea what I am talking about, you must be an American, or possibly Canadian, or have never played football.)  Creating overloads that contain optional arguments is almost never a good idea.

Consider the following code, and determine which method overload gets called.
string Greeting(
    string greeting            // Required
    ,string subject = "world") // Optional
{
  return string.Format("{0}, {1}!", greeting, subject);
}

string Greeting(
    string subject = "world")  // Required
{
  return string.Format("Hello, {1}!", subject);
}

string Greeting(
    string greeting            // Required
    ,string subject = "world"  // Optional
    ,string punctuation = "!") // Optional
{
  return string.Format("{0}, {1}{3}"
      ,greeting
      ,subject
      ,punctuation);
}

void CallGreeting()
{
  Greeting("Good night", "Gracie", ".");
  Greeting("Good night", "Gracie");
  Greeting("Good night");
}

First, be aware that the compiler happily processes the above code, producing no errors.  The application runs, too.  Now, let's see where these calls land:
  1. The first call in the CallGreeting method obviously gets routed to the third overload
  2. The second call is routed to the first method
  3. The thrid call is routed to the second method
Calls are routed to the overload containing the matching number of parameters we provide.  This can be very problematic, if we introduce an overload to existing code!  Calls to an existing method, the third overload, will suddenly get routed to the new, second overload, when only 2 values are provided in the call.

SOLUTIONS

The solution to this issue is to simply not have any overloads, and use only optional arguments; or, if the overloads perform fundamentally different actions, rename the overload method altogether, and not have an overload.  For example, create a method named FormattedGreeting(), instead of creating an overload.

The Versioning Trap

Phil Haack astutely observed in his blog entry, More Versioning Fun With Optional Arguments, you may also encounter a situation where an overload is added that has required arguments.  Appologies to Phil, for my borrowing his example code:

Source Code Version 1
public static void Foo(string s1, string s2, string s3 = "v1") {
    Console.WriteLine("version 1");
}

Calling ClassName.Foo("one", "two") returns "version 1".


Source Code Version 2
public static void Foo(string s1, string s3 = "v2") {
  Console.WriteLine("version 2");
}

public static void Foo(string s1, string s2, string s3 = "v1") {
  Console.WriteLine("version 1");
}

Calling ClassName.Foo("one", "two") now returns "version 2".  Uh ho.  The compiler still gives preference to matching required parameters over matching number of arguments to a method that has optional parameters.

Summary

Take great care when writing methods that use optional arguments.  Knowing compiler preference will help avoid pitfalls, and help with future debugging, particularly in partial classes, where this issue is most likely to be encountered.  Some basic architectual planning should alert you to possible future bear traps, and maintaining (and running) unit tests will ensure you haven't been ensnared.

No comments:

Post a Comment

Please provide details, when posting technical comments. If you find an error in sample code or have found bad information/misinformation in a post, please e-mail me details, so I can make corrections as quickly as possible.