C#: Optional Parameters - Pros and Pitfalls
- by James Michael Hare
When Microsoft rolled out Visual Studio 2010 with C# 4, I was very excited to learn how I could apply all the new features and enhancements to help make me and my team more productive developers.
Default parameters have been around forever in C++, and were intentionally omitted in Java in favor of using overloading to satisfy that need as it was though that having too many default parameters could introduce code safety issues. To some extent I can understand that move, as I’ve been bitten by default parameter pitfalls before, but at the same time I feel like Java threw out the baby with the bathwater in that move and I’m glad to see C# now has them.
This post briefly discusses the pros and pitfalls of using default parameters. I’m avoiding saying cons, because I really don’t believe using default parameters is a negative thing, I just think there are things you must watch for and guard against to avoid abuses that can cause code safety issues.
Pro: Default Parameters Can Simplify Code
Let’s start out with positives. Consider how much cleaner it is to reduce all the overloads in methods or constructors that simply exist to give the semblance of optional parameters. For example, we could have a Message class defined which allows for all possible initializations of a Message:
1: public class Message
2: {
3: // can either cascade these like this or duplicate the defaults (which can introduce risk)
4: public Message()
5: : this(string.Empty)
6: {
7: }
8:
9: public Message(string text)
10: : this(text, null)
11: {
12: }
13:
14: public Message(string text, IDictionary<string, string> properties)
15: : this(text, properties, -1)
16: {
17: }
18:
19: public Message(string text, IDictionary<string, string> properties, long timeToLive)
20: {
21: // ...
22: }
23: }
Now consider the same code with default parameters:
1: public class Message
2: {
3: // can either cascade these like this or duplicate the defaults (which can introduce risk)
4: public Message(string text = "", IDictionary<string, string> properties = null, long timeToLive = -1)
5: {
6: // ...
7: }
8: }
Much more clean and concise and no repetitive coding! In addition, in the past if you wanted to be able to cleanly supply timeToLive and accept the default on text and properties above, you would need to either create another overload, or pass in the defaults explicitly. With named parameters, though, we can do this easily:
1: var msg = new Message(timeToLive: 100);
Pro: Named Parameters can Improve Readability
I must say one of my favorite things with the default parameters addition in C# is the named parameters. It lets code be a lot easier to understand visually with no comments. Think how many times you’ve run across a TimeSpan declaration with 4 arguments and wondered if they were passing in days/hours/minutes/seconds or hours/minutes/seconds/milliseconds. A novice running through your code may wonder what it is. Named arguments can help resolve the visual ambiguity:
1: // is this days/hours/minutes/seconds (no) or hours/minutes/seconds/milliseconds (yes)
2: var ts = new TimeSpan(1, 2, 3, 4);
3:
4: // this however is visually very explicit
5: var ts = new TimeSpan(days: 1, hours: 2, minutes: 3, seconds: 4);
Or think of the times you’ve run across something passing a Boolean literal and wondered what it was:
1: // what is false here?
2: var sub = CreateSubscriber(hostname, port, false);
3:
4: // aha! Much more visibly clear
5: var sub = CreateSubscriber(hostname, port, isBuffered: false);
Pitfall: Don't Insert new Default Parameters In Between Existing Defaults
Now let’s consider a two potential pitfalls. The first is really an abuse. It’s not really a fault of the default parameters themselves, but a fault in the use of them. Let’s consider that Message constructor again with defaults. Let’s say you want to add a messagePriority to the message and you think this is more important than a timeToLive value, so you decide to put messagePriority before it in the default, this gives you:
1: public class Message
2: {
3: public Message(string text = "", IDictionary<string, string> properties = null, int priority = 5, long timeToLive = -1)
4: {
5: // ...
6: }
7: }
Oh boy have we set ourselves up for failure! Why? Think of all the code out there that could already be using the library that already specified the timeToLive, such as this possible call:
1: var msg = new Message(“An error occurred”, myProperties, 1000);
Before this specified a message with a TTL of 1000, now it specifies a message with a priority of 1000 and a time to live of -1 (infinite). All of this with NO compiler errors or warnings.
So the rule to take away is if you are adding new default parameters to a method that’s currently in use, make sure you add them to the end of the list or create a brand new method or overload.
Pitfall: Beware of Default Parameters in Inheritance and Interface Implementation
Now, the second potential pitfalls has to do with inheritance and interface implementation. I’ll illustrate with a puzzle:
1: public interface ITag
2: {
3: void WriteTag(string tagName = "ITag");
4: }
5:
6: public class BaseTag : ITag
7: {
8: public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
9: }
10:
11: public class SubTag : BaseTag
12: {
13: public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
14: }
15:
16: public static class Program
17: {
18: public static void Main()
19: {
20: SubTag subTag = new SubTag();
21: BaseTag subByBaseTag = subTag;
22: ITag subByInterfaceTag = subTag;
23:
24: // what happens here?
25: subTag.WriteTag();
26: subByBaseTag.WriteTag();
27: subByInterfaceTag.WriteTag();
28: }
29: }
What happens? Well, even though the object in each case is SubTag whose tag is “SubTag”, you will get:
1: SubTag
2: BaseTag
3: ITag
Why? Because default parameter are resolved at compile time, not runtime! This means that the default does not belong to the object being called, but by the reference type it’s being called through. Since the SubTag instance is being called through an ITag reference, it will use the default specified in ITag.
So the moral of the story here is to be very careful how you specify defaults in interfaces or inheritance hierarchies. I would suggest avoiding repeating them, and instead concentrating on the layer of classes or interfaces you must likely expect your caller to be calling from.
For example, if you have a messaging factory that returns an IMessage which can be either an MsmqMessage or JmsMessage, it only makes since to put the defaults at the IMessage level since chances are your user will be using the interface only.
So let’s sum up. In general, I really love default and named parameters in C# 4.0. I think they’re a great tool to help make your code easier to read and maintain when used correctly.
On the plus side, default parameters:
Reduce redundant overloading for the sake of providing optional calling structures.
Improve readability by being able to name an ambiguous argument.
But remember to make sure you:
Do not insert new default parameters in the middle of an existing set of default parameters, this may cause unpredictable behavior that may not necessarily throw a syntax error – add to end of list or create new method.
Be extremely careful how you use default parameters in inheritance hierarchies and interfaces – choose the most appropriate level to add the defaults based on expected usage.
Technorati Tags: C#,.NET,Software,Default Parameters