Optional arguments on both ends
Before we get into today's topic, a quick update on my posting from last year about Roslyn jobs. We have gotten a lot of good leads and made some hires but we still have positions open, both on the Roslyn team and on the larger Visual Studio team. For details, see this post on the Visual Studio blog. Again, please do not send me resumes directly; send them via the usual career site. Thanks!
Here's a recent question I got from a reader: what is the correct analysis of this little problem involving optional arguments?
static void F(string ham, object jam = null) { }
static void F(string spam, string ham, object jam = null) { }
...
F("meat product", null);
The developer of this odd code is apparently attempting to make optional parameters on both ends; the intention is that both spam and jam are optional, but ham is always required.
Which overload does the compiler pick, and why? See if you can figure it out for yourself.
.
.
.
.
.
.
.
.
.
.
.
.
.
The second overload is chosen. (Were you surprised?)
The reason why is straightforward. The overload resolution algorithm has two candidates and must pick which is the best one on the basis of the conversions from the arguments to the formal parameter types. Look at the first argument. Either it is converted to string, or to... string. Hmph. We can conclude nothing from this argument, so we ignore it.
Now consider the second argument. We either convert null to object, if we pick the first overload, or to string, if we pick the second overload. String is obviously more specific than object; every string is an object, but not every object is a string. We strive to pick the overload that is more specific, so we choose the second overload, and call
F("meat product", null, null);
The moral of the story is, as I have said so many times, if it hurts when you do that then don't do that! Don't try to put an optional argument on both the front and back ends; it's just confusing as all heck, particularly if the types collide as they do here. Write the code as a single method with each optional parameter declaration coming at the end:
static void F(string ham, string spam = null, object jam = null) { }
With only one overload it is clear what argument corresponds to what parameter.
Now perhaps you see why we were so reticent to add optional arguments to overload resolution for ten years; it makes the algorithm less intuitive in some cases.
Comments
Anonymous
February 10, 2011
The comment has been removedAnonymous
February 10, 2011
Is your example wrong? Shouldn't it be: static void F(string spam = null, string ham, object jam = null) { }Anonymous
February 10, 2011
@Chris: That would be syntactically incorrect, as optional arguments cannot come before any non-optional arguments.Anonymous
February 10, 2011
The comment has been removedAnonymous
February 10, 2011
On the other hand: if you kept them out so long, why did you guys let them in in c#4.0 then? #genuinelycurious There are legacy object models out there (I'm thinking specifically of the Office object model, but there are of course others as well) that were designed for use with VB 6 and earlier. Since VB has always supported optional arguments, many of those object models were difficult to use from C#. We have failed to move the mountain; those object models have not been rewritten to conform to the C# API design model in the last ten years. If the mountain won't come to you, you have to go to the mountain; enough customers told us that it was a major pain point to work with these important object models that we finally gritted our teeth and added the feature. - EricAnonymous
February 10, 2011
Who cares if it makes overload resolution less obvious? The people who design, implement, test and maintain the language care. The people who have to design and implement and test new language features on top of an already-too-complicated language semantics care. Customers whose programs are broken by the breaking changes introduced by this feature care. (The feature does introduce breaking changes; the example I give in this article was motivated by a question from a customer whose C# 3 code broke when they ported it to C# 4.) Customers whose programs do not work correctly because overload resolution silently chose an unexpected method care. Lots of people care. - Eric Optional parameters on constructors are critical to writing succinct immutable code. Imagine you have a Person class with dozens of different properties. Since you can write: new Person { FirstName = "Eric", LastName = "Lippert", HomeTown = "Waterloo" } ony when Person is mutable, the old way was to have many different constructor overloads, something messy like: new ImmutablePerson().WithFirstName("Eric").WithLastName("Lippert").WithHomeTown("Waterloo") which creates 3 extra Person objects that you don't need, or a single constructor that takes all possible parameters like: new ImmutablePerson(null, null, null, "Lippert", null, null, "Eric", null, null, null, null, null, "Waterloo", null, null, null, ...) Using optional parameters lets you write: new ImmutablePerson(FirstName: "Eric", LastName: "Lippert", HomeTown: "Waterloo") I agree. In C# 3 we swung the pendulum both ways: we encouraged a more immutable style of programming with monadic query comprehensions and immutable expression trees, but also encouraged a more mutable style of programming with object and collection initializers. We are thinking hard about ways to swing that pendulum more towards the immutable side of things; we don't want to make it more difficult to program in an immutable style. However, this factor was of only secondary importance as far as the optional arguments feature was concerned. In order to address the needs of people who want to program in an immutable style, we really should be considering features that directly address that concern, rather than merely doing incremental improvements around the periphery, like this feature. - EricAnonymous
February 10, 2011
@Gabe Or: new ImmutablePerson(new Person { etc } ); Still messy of course, but it doesn't have to be as bad as your example.Anonymous
February 10, 2011
The comment has been removedAnonymous
February 10, 2011
@S: But before, it was still possible to define a new method with the additional argument, and have the old method call the new one.Anonymous
February 10, 2011
The comment has been removedAnonymous
February 10, 2011
>it makes the algorithm less intuitive in some cases. Imho, best solution is throw compile error in such cases.Anonymous
February 10, 2011
The comment has been removedAnonymous
February 10, 2011
>How can the calling of a function with three parameters, of which one is NOT provided, be more specific than the version with two parameters which is an exact match based on the numer of parameters given? Because string is more specific than an object? So, if we swap the types of the second parameters, as fallows: static void F(string ham, string jam = null) { } static void F(string spam, object ham, object jam = null) { } then the compiler will pick the first overload? Lately I've been thinking of migrating to C#, but have near zero experience and am not sure at all if I got this right...Anonymous
February 10, 2011
The comment has been removedAnonymous
February 11, 2011
Overload resolution when combined with default valued parameters was a difficult to grasp concept in C++ too. Add Koenig lookup and you could lose your sleep trying to figure out what was going on. In 3.0, second F gets called void F( string a , object b ) { } void F( string a , string b ) { } So another (perhaps imprecise) way to see why it does what is does is: algorithm picks up the best version using all provided values ignoring any optional parameters that may follow. Incidently, what happens when I do this? (VS 2010 will take a long time to fire up, not goint to do that.) static void F(string ham, object jam = null) { } static void F(string spam, string ham, object jam = null) { } static void F(string spam, string ham, object jam = null, object bread = null) { } ... F("meat product", null); Should be ambiguous match I guess.Anonymous
February 11, 2011
Please add an option no-optional-parm to let us turn them off at the complier level.Anonymous
February 11, 2011
The comment has been removedAnonymous
February 12, 2011
Eric, please bring back your stylish look and feel!!Anonymous
February 13, 2011
So I understand the madness optional arguments can lead to. But why disallow them for so long and yet allow goto statements?Anonymous
February 13, 2011
Changing the subject. Did anyone noticed that the blog text color is no longer purple?? This must be some kind of the end of an era in this blog historyAnonymous
February 14, 2011
I've just seen that the purple text is back! So maybe the issue was fixed, or it was something wrong with my browser's cache A number of people reported to me that the purple text disappeared on Friday; many thanks. In fact, all of my customizations got turned off. Something was misconfigured in the blog settings; it is restored now. -- EricAnonymous
February 28, 2011
Just add a compile time warning for ambiguous situations like this. That doesn't help because the problem is that it is not ambiguous! If it were ambiguous then it would be an error, not a warning. The problem is that the unambiguous best choice is possibly not the one that the user intended. But it is always the case that overload resolution could be choosing the method that the user did not intend. We can't go putting a warning on every usage of overload resolution that could possibly be wrong, because that's all of them. And suppose we did add a warning; what is the developer supposed to do to eliminate the warning? The point of the article is that this is a bad way to design overloads; design your overloads to work well with the overload resolution algorithm so that developer's intuitions more clearly match the meaning of the code. - EricAnonymous
March 05, 2011
I tend to avoid overloading methods in a way that there is a set of parameter types that is compatible with several overloads which have different semantics. Unfortunately not everybody has the same opinion. ASP.net MVC is guilty of that in some places too. For example the rendering of a view as several overloads, in particular two with the same parameter count. With the only difference being that one takes a model as object, and the other takes some name as string. And once I was lazy and decided to use string as my model class...