Visual Studio 2010 has been launched, and thus C# 4.0 has become the latest standard-de-facto in the world of .Net development.
Previous major modification of the language has brought us many attractive features, which no doubt could significantly increase the development efficiency and simplify our life. So, it is quite predictable that all C#-developers were anticipating finding out even more improvements in the new release.
The release is here, and we’ve finally got three brand-new features: dynamic binding, optional and named arguments and the variance annotations. While the first and the last are specifically designed for a custom set of use-cases and scenarios, the optional and named arguments look like the simple time saving improvement, intended to be widely used in almost any C# code. They do not add any new functionality or technology – they just simplify things that we have been always doing. This is what is usually called “syntax sugar”, and such sugar features usually tend to become extremely popular, so most probably very soon we will see a lot of code written with heavy usage of optional arguments.
Before I continue, let me briefly describe the feature itself. It is quite obvious and easy, and has been used in many other programming languages for ages; however it is new in C#-world, so here it is. If you already know what are the optional arguments, then simply skip the next section.
“New” feature
Imagine a situation when you need to pass default values to the method or an object constructor. There may be different sets of defaults values for different scenarios. What would you do? Right, you would create a set of method overloads, do all the actual logic in the body of the method having most complete and specific set of the arguments, and call this method from other methods, substituting the default argument values in that calls. It would look something like this:
If the number of possible overrides is large, then the code becomes ugly and difficult to read and maintain. C++ and VB developers would use default arguments syntax here – and finally we’ve got a possibility to do it as well:
In this case arg1 is a required parameter, while arg2 and arg3 are optional parameters. So, the usage is obvious: calling DoSomeMethod(11) is equivalent to calling DoSomeMethod(11,0,12). In the same fashion you can explicitly specify the value of arg2 and skip only arg3.
You cannot omit first optional parameter while explicitly specifying the last one – this would significantly degrease the readability of such code (imagine the need to count commas in some multi-parametric method). Because of that, you cannot call anything like DoSomeMethod(11,,12) – this will lead to a compile-time error.
Another part of the new feature – so called “named arguments” comes to help in this case. You may explicitly specify the name of the parameter when entering argument values, which will allow you to enter them in any given order (probably, different from the order in which they are declared) and omitting any optional parameters. Like this:
That’s easy and simple, right? Most of the people who see this feature immediately like it and most probably are going to use it in their code. But some details remain hidden, and even reading Microsoft’s C# specification may not clear them all. And the details – as always – are the residence of the Devil.
Fun begins
Let’s start with the simple case.
Imagine we have the following code:
What do you expect to see as an output? Which method will be called? Or will we get a compilation error?
As for me, I would really prefer to see the compilation error here, because this really looks as an ambiguity. However, it is not: no error appears, the code compiles and executes successfully, and we see that a parameterless method is called. This happens because the member which matches the exact list of arguments is considered better then the one which matches the list of required arguments only. So, there is a reasonable explanation, yet this behavior still may very confusing.
Ok, let’s move on, to a more complicated case: inheritance and virtual methods.
Here we have a virtual method with an optional argument, its override in the derived class and a new method with no parameters defined in the derived class only. So, what will happen if we execute the following code:
This is actually the same situation as we had in previous example: parametersless method is a closer match then the one with the optional arguments, so we’ll get “Derived called” output.
But notice, that if you declare o as Base, you’ll get completely different story: call to Foo() will become a call to the virtual method of Base, and so the method with the optional argument will be executed: you’ll get “Overriden called” as an output. This is expected behavior, at least if you understand how virtual members are executed.
But now let’s turn the thing inside out: let’s move a parameterless method to Base and make it virtual, while putting the Foo(string bar = null) inside the derived class:
Can you guess what will happen now if we call the same code?
According to what we’ve seen before, we may expect that the overridden method will be called, because its signature is a closer match. However, it is not! We see a “Derived called” output, which means that in this particular case an overload was resolved in favor of a method with optional argument, which is non-virtual and is defined only in Derived.
Sounds crazy, doesn’t it?
The only explanation why this happens which I may suggest is that the search for the best match has a preference for methods explicitly defined in this particular class for which the call is made. So, if you put a new keyword instead of override (or simply omit this keyword, which is a bad practice, but means the same), you’ll get back on track: a parameterless version of the method will be called. But as soon as the method becomes virtual – bang! – it gets less priority when compared with others.
Conclusion
There is no clear clue for this behavior in MSDN, and even C# specification does not tell this explicitly. So, this is really, really dangerous pitfall. Imagine a vast code refactoring which introduces new method overloads and changing the behavior of existing methods... This may lead to a real pain.
So, be careful when using optional arguments and try to obey the following simple rules to minimize ambiguity:
- When introducing a new method with optional parameters, remove old method overloads which were used for the same purpose before.
- When overriding some method with optional parameters make sure that there are no new methods of the same signature at this class
- When creating a new method in a class, make sure that it has different name with the optional-argument methods defined in the base class even if their argument signatures are different.
Personal note as a bottom-line
When C# first appeared, it was a clear and easy-to-learn & easy-to-use language, with minimum number of assumptions and ambiguous rules, implicit casts and overloads. It didn’t require you to know hundreds of hidden dangers and pitfalls; there were not features which were available only theoretically, but practically lead to bad coding practices and so on.
Now – when more and more “syntax sugar” extensions come out – the language is moving fast towards being complex, universal, but very generally defined tool, which has many unwanted surprises for unprepared developer.
Probably, this is a logical way of the maturity of any development tool or product. However, I personally feel little bit sad about that. The time of clarity and formality in the C#-world seems to be passing away.
Labels: .NET, .Net 4.0, C#, ~Alexander Tivelkov