Named arguments and overload resolution

Last time we talked about the basics of named arguments, optional arguments, and default values. From here on out, I’m just going to refer to the whole feature group as “named and optional arguments” – it’s just too much typing otherwise (we actually just refer to the feature as N&O internally). Let’s now dive a little deeper into how overload resolution works for the feature.

Where do we get those wonderful names?

The first thing we need to figure out, is where we get the names from. Because the CLR does not consider parameter names as part of the method signature, it is perfectly legal to override a method and specify different parameter names than the base method that one is overriding. Lets consider the following:

This is perfectly legal C# code. So then which names do we pick? Do we pick the ones from the base method? Or the most derived one? What about interfaces? Let’s consider a usage of named arguments with this example.

If we consider the receiver (aka the calling object/type) as the anchor in this whole scheme, and use its statically determined type to figure out the names, then we’ve got ourselves a nice little scheme that is deterministic and quite sensible.

In our example then, because m is statically typed to be of type Monkey, m.Eat gets the names of the parameters from the Monkey class, and so bananaType is the correct name to be using. Similarly, a is typed Animal, and so the call to a.Eat gets the name foodType as its parameter. Notice that even though their runtime types will be identical (that is, we’ve taken an instance of Monkey and assigned it into an Animal local variable), that doesn’t matter – the named parameter feature is simply a syntactic sugar for a compile time rewrite.

What actually happens under the covers here? When the compiler sees the call to list.SearchForContacts, it first performs a few quick validations to make sure that any positional arguments (arguments not specified by name) occur before any named arguments, and that no names are specified twice. Then it generates a set of all applicable candidates. In our example, there is but one candidate to consider. Then for each candidate, the compiler looks at the arguments, and performs a few verifications.

First it checks to make sure that the names specified in the call (in our case, “age”) is valid for the candidate (ie the candidate has a parameter named “age”). Next, the compiler moves past all positional arguments attempts to match each named argument up with its corresponding parameter. Of course, all named arguments must match parameters who do not have a positional argument specified for it (ie you cannot specify an argument for the same parameter twice).

For each parameter that does not have a corresponding positional or named argument, the compiler checks to make sure it is optional. It then uses the default parameter value for each of those arguments.

Once the compiler has generated this augmented argument list, it performs argument convertibility on it as usual. The resulting augmented list in our example then, is: “any”, 26, “any”.

Just compiler magic

I love magic. I love the concept of magic. It amazes me. I went to Disneyland recently for PDC, and it was magical. I know it isn’t real, and that something else is happening, but I love it anyway. That’s what named arguments are like. Underneath the covers, there is no trace of named (or optional) things – it all looks like straight IL, as if you called the methods with the augmented argument list.

That means that after the compiler has generated the augmented list for you and has found the best candidate, it treats the call as if you had called it with the augmented list, and everything else behaves as it used to.

That being said, it means that this feature is totally a compile time syntactic sugar, and so it doesn’t introduce any new dependencies, and doesn’t introduce any new compatibility issues or anything like that. Programs that you compile against one set of names and default values will absolutely continue to keep working even if the library changes and has a new set of names and default values.

One more piece of magic

Turns out there’s one more crucial thing that the compiler does for you, which is really worth mentioning. Order of evaluation. Lets consider the following example:

What happens here? Well, the compiler will reorder the arguments so that the result of Bar() gets passed as the first argument, the result of Baz() gets passed as the second, and the result of Foo() gets passed as the third. However, what order should these sub-expressions be evaluated? You’d expect them to be evaluated as written – Foo first, then Bar, then Baz.

And that’s exactly what we do. We essentially create temporaries which store the value of evaluating each of those sub-expressions (which means that all side effects of evaluating each expression happen in syntax order as you’d expect), and reorder those temporaries to match their respective named positions. See? Magic!

So hopefully that gives you a good feel for how the feature works, and how overload resolution for it works. Have fun with it, and definitely give us your feedback!

So what happens here? Will the compiler decide to only look for 1-argument Bar(), find Bar(char), and then complain about type mismatch? Or will it first consider all Bar overloads that have an argument named "z" (thus discarding Bar(int, string) at that point), and then do overload resolution as usual based on types?

Another interesting question is how override works in conjunction with "dynamic" – i.e. in case of conflicting argument names between original method and override(s), what takes precedence. According to the logic of "dynamic", I would expect it to behave as if "dynamic" was replaced by actual runtime type of object at the point of the call – is that right?

The compiler will first find the 1-argument Bar, decide that the candidate is not valid because the conversion doesn’t work, and then will find the two-argument Bar, find that the first parameter has a default value, and the second parameter has a matching name "z", and the argument specified is convertible so the compiler will bind to that.

When you’ve got a dynamic argument, the whole call gets bound dynamically, and the same overload resolution rules happen at runtime with the names and optional values. So yes, you’re exactly right 🙂