With all of the new C# 4.0 stuff coming out, I feel like a kid in a candy store. Sorry for post overload, but I just can’t help myself! I also think I need to stop putting numbers on these posts, because obviously I have just completely thrown them to the curb. I hate to put a "Part 3" on this post though, since it is just an extension of a previous one.
Jonathan Pryor pointed out on my last post that the default parameter feature in C# 4.0 was implemented in the same way that that the default parameter feature in VB.net has been implemented. He also points out a seemingly obvious way that they could have made it better, but then he points out why it wouldn’t work when combined with the named parameter feature.
So, since Jonathan is a freakin’ smart guy, and most of us (including me) aren’t that smart, I am going to elaborate on his comment and explain in detail what he is talking about.
So, to start off let’s look at the implementation of default parameters in C# 4.0. It all starts with two attributes called OptionalAttribute and DefaultParameterValueAttribute. If you go look these attributes up, you will see that they have been around since since .net 1.1 and .net 2.0 respectively. The reason for this is that other languages besides C# have supported these features going back to .net 1.1. In fact, you could add a DefaultParameterValueAttribute to one of you parameters in a method in C# and it would work perfectly fine, it is just that you can not consume it in C# since C# does not support this feature (until C# 4.0).
In my previous post I created a class that looked like this:
public class TestClass { public void PerformOperation(string val1 = "val", int val2 = 10, double val3 = 12.2) { Console.WriteLine("{0},{1},{2}", val1, val2, val3); } }
So, you see that this class has a method which has three default parameters. This means that we can call this method without passing any arguments to it and the default values would be "filled in" for us. So, how does the C# compiler implement this behavior?
Your first idea may be that C# just generates overloads, something that looks like this:
public void PerformOperation() { PerformOperation("val", 10, 12.2); } public void PerformOperation(string val1) { PerformOperation(val1, 10, 12.2); } public void PerformOperation(string val1, int val2) { PerformOperation(val1, val2, 12.2); } public void PerformOperation(string val1, int val2, double val3) { Console.WriteLine("{0},{1},{2}", val1, val2, val3); }
Well, in reality, it looks like this (this is reflected code):
public void PerformOperation([Optional, DefaultParameterValue("val")] string val1, [Optional, DefaultParameterValue(10)] int val2, [Optional, DefaultParameterValue(12.2)] double val3) { Console.WriteLine("{0},{1},{2}", val1, val2, val3); }
Hmmmmm. So, instead of just generating overloads for each method with the values filled in, it just applies some attributes to the parameters that declare them as optional and then specifies their default values. But, how does that work?
If you are familiar with attributes in .net then you will know that you have to use reflection to read out the properties of these attributes, and you have to have code running somewhere to process these attributes. All they are is meta-data assigned to the method, not code that executes at runtime. So, is C# doing reflection every time I call a method with default parameters? Fortunately the answer to that question is "no".
The answer to how this works may be a little bit surprising though. If we want to call the above method with no parameters:
var testClass = new TestClass(); testClass.PerformOperation();
What does this compile to? Interestingly it looks like this:
var testClass = new TestClass(); testClass.PerformOperation("val", 10, 12.2);
You’ll notice that the default parameter values for this method have just been compiled right into the calling code. The C# compiler is reading those attributes off the method and then using them to just insert the values into the calling code and then compiling them. So, what happens if I change the default values and don’t compile my entire system? Well, the calling code will still have the wrong values. That is definitely something that you will have to look out for.
So, why did they choose to implement it this way? Well, as Jonathan pointed out, if they dynamically generated overloads one thing that wouldn’t work is the new named parameters feature. Why wouldn’t it work? Well, I’m glad you asked.
Lets say we had the overloaded methods that I put in above, and I wanted to call my method like this:
var testClass = new TestClass(); testClass.PerformOperation(val3: 15.1);
Hmmm. What overload would I call? I don’t have an overload to call. Even though we generated overloads, we still can’t leave out parameters and we would be stuck with inserting values into our IL again. Then we would have a mixed system where sometimes it would bake in values, and other times it wouldn’t. No good.
Now, you might say, what about just generating overloads for all parameters in all orders? Well, since we have three parameters of different types, that would work for our particular instance. It would not work for all instances though. What if we had three string parameters? You can’t have three overloads of a method that each take three strings, method resolution would be impossible.
It appears for now that these two features just won’t interact, and I’m sure that if there was a way in the current .net runtime to make it work without baking in the values, they would have. But for now we just have to accept the way it works and move on. Maybe in the future the runtime will have a way to tag parameters with default values that can stay with the method and then use those values when parameters aren’t provided. Who knows. Hopefully you found this little adventure into the default parameter to be interesting, and hopefully you’ll come back for part 3 which will be coming along shortly.
Loved the article? Hated it? Didn’t even read it?
We’d love to hear from you.
Who says the overloads actually have to be [i]overloads[/i]? They could simply be generated methods like this:
[quote]public void PerformOperation<0>() {…}
public void PerformOperation<1>(string val1) {…}
public void PerformOperation<2>(int val2) {…}
public void PerformOperation<3>(string val1, int val2) {…}
public void PerformOperation<4>(double val3) {…}
etc. [/quote]
with some kind of attribute to notify the compiler that PerformOperation with any less than all parameters compiles into one of the implementations.
What if you have two assemblies, one has a public method with default parameters, the other assembly calls that method, build both of them, then go back and change the default values for your parameters and rebuild only that one assembly? Would, the old assembly still be calling that method with the old parameters? If so (which it sounds like), these default parameters could cause some headaches in multi-assembly apps
@Adam. Default parameters are saved in the assembly they were declared in. They are not like Constants which can get cached in a calling assembly. So simply updating the assembly that contains the methods with default parameters will change what the defaults are.
–My Comments–
Default/Optional/Named parameters have been supported in the CLR since the beginning of .NET. VB has had them the whole time. C# only exposed them in attributes until now. Adding attributes onto parameters is how MSIL has done it since 1.0.
While building overloads works in the C# frame of mind it doesn’t when doing language interop with VB, Python, Ruby.
If you look at all the new features in C# 4.0, they all have one thing in common. They are required for dynamic language interop. While I’m extremely excited for having the dynamic support.
I was a bit bummed we didn’t get new syntax for things like parallelism (like F#’s async block). The new parallel libraries are very nice though. Tasks finally put a good framework on top of the Elephant in the room.
I guess I was wrong. I built a quick test of the named/optional parameters. The default values do get cached in the calling assembly.
The method with default params:
public static void DoSomething(string Name = "Mordrid", int Age = 47)
C# calling line
Class1.DoSomething();
MSIL Generated
.method private hidebysig static void CallSomething1() cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldstr "Mordrid"
L_0006: ldc.i4.s 0x2f
L_0008: call void [NamedParametersLibrary]NamedParametersLibrary.Class1::DoSomething(string, int32)
L_000d: nop
L_000e: ret
}
@Jeff, thanks for the response. The article makes it sound like the calling assembly does have the values compiled into it:
(From the article)
1. var testClass = new TestClass();
2. testClass.PerformOperation();
What does this compile to? Interestingly it looks like this:
1. var testClass = new TestClass();
2. testClass.PerformOperation("val", 10, 12.2);
(/FTA)
Doesn’t that mean it’s in the calling assembly?
Thanks!
See my comment above yours. It does get cached, which is unfortunate. The funny thing is though the assembly with the default params does cache them as well
Here’s the MSIL of the method I used in my examples.
.method public hidebysig instance void DoSomething([opt] string Name, [opt] int32 Age) cil managed
{
.param [1] = string(‘Mordrid’)
.param [2] = int32(0x2f)
.maxstack 8
L_0000: nop
L_0001: ldstr "{0} is {1} years old"
L_0006: ldarg.1
L_0007: ldarg.2
L_0008: box int32
L_000d: call void [mscorlib]System.Console::WriteLine(string, object, object)
L_0012: nop
L_0013: ret
}
So it does load the defaults right away. but the console app I wrote would save the values that existed previously.
I tried letting this be binded at runtime with the dynamic keyword but I got a runtime exception that no overload for DoSomething() has 0 parameters.
Also I just tested changing the assemblies default values without rebuilding the console application. The console app did print out the previous default values.
This definitely could have large impacts on the way people write their code, it goes against what one would think would happen. Lets hope this is fixed before release (we aren’t even in Beta yet)
I wonder how VB has been handling this all these years.
VB does the exact same thing. I wrote my library and console in VB and the MSIL came out nearly identical. The values are cached in the calling assembly.
@Adam @Jeff Yep, sorry I didn’t make that clear in my post above. The values are "baked" into the calling code. This means that any changes to the default values won’t affect calling assemblies until they are recompiled.
Whats funny is this is how it’s worked from the beginning. It seems like no one’s had an issue with it so far. Still it’s far from what one would expect. I guess the rule of thumb (as with Constants) is if you are going to be exposing your library for public consumption, avoid optional/default parameters.
I always found it funny that constants were baked in as well, there’s barely any performance benefit out of it.
For anyone that’s confused by all this, CLR via C# is an excellent book to read. (I’m hoping for a CLR 4.0 update)
So, left out parameters fill in the current default value at compile time. What about Office interop? I’ve read somewhere that the new dynamic features can be used for Office interop with different Office versions, like 2003, 2007, without having to compile for each version. Also, especially Office methods have tons of parameters so you could well use the named parameters, which includes optional parameters to make it useful. (Mostly; if not for code readability.) What if a default value changes from one to the next Office version? Where does it get the default value from, anyway, if the actual object is only known at runtime? Will it then look at that class metadata and fetch its default parameter value at runtime for dynamic objects?
@Yves I haven’t done a lot with COM interop, so I can’t really answer your questions. What I do know is that the dynamic keyword in C# 4.0 will allow for you to reference COM dlls without PIAs and then call them like normal methods on a C# object and default parameters work like you’d expect. I’m not sure of the detail much deeper than that, sorry.
Yves, the default value for COM interop is always either a zero value for numbers or Missing.Value for objects. The external dlls know what to do with a missing value – make it the internal default value – but since what is actually sent is Missing.Value, that is the only thing your assemblies are ‘baked’ with.