Optional argument corner cases, part four

(This is the fourth and final part of a series on the corner cases of optional arguments in C# 4; part three is here.)

Last time we discussed how some people think that an optional argument generates a bunch of overloads that call each other. People also sometimes incorrectly think that

void M(string format, bool b = false)
{
Console.WriteLine(format, b);
}

is actually a syntactic sugar for something morally like:

void M(string format, bool? b)
{
bool realB = b ?? false;
Console.WriteLine(format, realB);
}

and then the call site

M("{0}");

is rewritten as

M("{0}", null};

That is, they believe that the default value is somehow "baked in" to the callee.

In fact, the default value is baked in to the caller; the code on the callee side is untouched and the caller becomes

M("{0}", false);

A consequence of this fact is that if you change the default value of a library method without recompiling the callers of that library, the callers don't change their behaviour just because the default changed. If you ship a new version of method "M" that changes the default to "true" it doesn't matter to those callers. Until a caller of M with one argument is recompiled it will always pass "false".

That could be a good thing. Changing a default from "false" to "true" is a breaking change, and one could argue that existing callers *should* be insulated from that breaking change.

This is a fairly serious versioning issue, and one of the main reasons why we pushed back for so long on adding default arguments to C#. The lesson here is to think carefully about the scenario with the long term in mind. If you suspect that you will be changing a default value and you want the callers to pick up the change without recompilation, don't use a default value in the argument list; make two overloads, where the one with fewer parameters calls the other.

(This is the fourth and final part of a series on the corner cases of optional arguments in C# 4; part three is here.)