Optional argument corner cases, part four

Optional argument corner cases, part four

Rate This
  • Comments 18

(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


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.)

  • Two thoughts:

    1.  Is the caller-rewrite behavior the same as VB.Net's behavior?  IOW, if I version a VB.Net class then call it from C# or vice versa, do I get consistent behavior?

    2. @Rob Manderson: With all respect, I think you didn't think quite enough.  At compile time for the callEE, it could easily have been transformed into the nested overloads model.  And then when the callER is compiled the appropriate generated callEE overload is selected.  And then also at run time when the compile-time type / run-time type issue is resolved.  As Eric pointed out in an earlier post, there are challenges with this approach.  But there's nothing which makes it logically impossible, or which makes "bake the callEEs default into the callER" inevitable.  Eric's decision to write 4 posts about this is IMO evidence the whole situation  is non-obvious.

  • @Shuggy:

    The problem with your scheme is that it means that programmer loses control over the public surface of the class. Now any language targeting CLR that does not implement that default-arg scheme (which would be all existing ones) suddenly sees a lot of weird public static members in other classes.

    Adding generated private members is fair game for the compiler. Public ones, not so much.

  • @Mark:

    Yes, the behavior is the same in VB. The only catch is that C# permits some changes to default values which are breaking to VB (on source level, of course, not binary). Specifically, in VB, if the base class changes the default argument for one of its virtual methods, then any VB overrides will stop compiling, whereas in C# they will compile just fine.

Page 2 of 2 (18 items) 12