Search
Close this search box.

C#/.NET Little Pitfalls: Default Parameters are Compile-Time Substitutions

C# is a wonderful language for modern programming.  While everything in C# has a reason and a place, occasionally there are things that can be confusing for a developer who isn’t aware of what is happening behind the scenes. This is my fifth post in the Little Pitfalls series where I explore these issues; the previous Little Pitfall post can be found here.

Side Note: I’ll be presenting sessions on the Little Wonders and the Little Pitfalls at the St. Louis Day of .NET conference on August 5th and 6th at the Ameristar Casino and Conference Center.  It’s a great mid-western tech conference and well worth the money!  Check them out here!

Default parameters have been around forever both in C++ and in VB.  When Java was introduced, however, they were eschewed as being a problematic source of potential code-safety issues. 

Having been bitten by default parameters before in less strongly-typed languages, I can understand this to an extent.  But in many ways this forced us to write heavier code (using overloads) where a default parameter would have been easier to maintain.  Java seemed to throw out the baby with the bathwater, just in my opinion, instead of letting the developer decide when using default parameters was the most appropriate choice. 

C# 1.0, having descended from Java and C++ roots, took the Java approach originally of not allowing default parameters.  With the advent of C# 4.0 compiler, however, default parameters are now a part of the C# language.  They are a great tool for reducing armies of overloads created to get around the lack of default parameters in early C#.  That said, they do have a few things to watch out for so you don’t get bit…

Overview of Default Parameters in C#

Default parameters let you specify a default value for a parameter of a method (or constructor) if the argument in that position isn’t provided.  Seems simple enough, right?  The main reason default parameters are nice is it eliminates the need for armies of overloads in constructors and methods.

Consider how much cleaner it is to reduce all the overloads in the constructors below 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:

public class Message {
  // can either cascade these like this or duplicate the defaults (which can
  // introduce risk)
  public Message() : this(string.Empty) {}

  public Message(string text) : this(text, null) {}

  public Message(string text, IDictionary<string, string> properties)
      : this(text, properties, -1) {}

  public Message(string text, IDictionary<string, string> properties,
                 long timeToLive) {
    // ...
  }
}

Now consider the same code with default parameters, reduced to one constructor:

public class Message {
  // can either cascade these like this or duplicate the defaults (which can
  // introduce risk)
  public Message(string text = "",
                 IDictionary<string, string> properties = null,
                 long timeToLive = -1) {
    // ...
  }
}

Very clean and concise!  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:

var msg = new Message(timeToLive: 100); 

Now, many of you will point out that we could have also achieved this using object initialization, like so:

var msg = new Message { TimeToLive = 100 };

But that is only true if the properties have a setter exposed.  Perhaps this Message class is designed to be immutable so once the message is created it can’t be changed.  In that case, we’d want to have multiple constructors (or one with default parameters) defined to give us the flexibility to construct in multiple ways, but not allow mutation after construction. 

Default parameters are a tool for cases like this, and of course for eliminating method overloads (which object initialization can’t help with).  That said, they do have some things to watch out for.

Pitfall: Optional parameter values are compile-time

There is one thing and one thing only to keep in mind when using optional parameters.  If you keep this one thing in mind, chances are you may well understand and avoid any potential pitfalls with their usage:

That one thing is this: optional parameters are compile-time, syntactical sugar!

So what do we mean by that?  Well, Basically just that optional parameters are compiler magic that helps the developer.  When you use optional parameters, the compiler looks first for a match for the method, and if it can’t find one it will look for a method that matches, taking default parameters into account and then it inserts the default values (just as if they were explicit in the code) into the compiled IL call to that method.

For example, say we had this snippet of code:

public class MyClass {
  public static void SomeMethod(int x = 5, int y = 7) {
    Console.WriteLine("Called with: {0}, {1}", x, y);
  }

  public static void Main() {
    SomeMethod(13);
  }
}

Notice the call to SomeMethod(13), this clearly passes in 13 for x and accepts the default of 7 for y, right?  Yes, but when does that default of y get set?  As you probably guessed from the title, these are set at compile-time. 

This means that these two calls will emit identical IL:

// the default parameters are substituted at compile-time!
SomeMethod(13);
SomeMethod(13, 7);

Now, since we know the default parameters are substituted at compile-time, this leads to our potential little pitfall: the default parameter used depends on the reference you are calling from and not the actual object (this is similar to our operator overloading pitfall we discussed here!)

So let’s illustrate this with a more complex example.  First let’s define an interface, a base class that implements the interface (virtually) and a sub-class that overrides that implementation.  And we’ll throw default parameters into the mix for fun!

public interface ITag {
  void WriteTag(string tagName = "ITag");
}

public class BaseTag : ITag {
  public virtual void WriteTag(string tagName = "BaseTag") {
    Console.WriteLine(tagName);
  }
}

public class SubTag : BaseTag {
  public override void WriteTag(string tagName = "SubTag") {
    Console.WriteLine(tagName);
  }
}

So, ITag defines WriteTag() with a default parameter of tagName = “ITag”, then BaseTag implements it with tagName = “BaseTag”and finally SubTag overrides it with tagName = “SubTag”.  Confusing eh? 

So now, what do we get when call it these three different ways:

public static void Main() {
  // three different typed references to same object instance
  SubTag subTag = new SubTag();
  BaseTag subByBaseTag = subTag;
  ITag subByInterfaceTag = subTag;

  // what happens here?
  subTag.WriteTag();
  subByBaseTag.WriteTag();
  subByInterfaceTag.WriteTag();
}

Some might expect them to all print “SubTag” since that’s the default parameter of the actual object instance for that method, right?  But hopefully you remembered the key that default parameters are compile-time magic.  That means the default parameter used is entirely dependent on the reference that the method is called from!

Thus, we will get:

SubTag
BaseTag
ITag 

This is because these two blocks of code emit the same IL, because the default taken is from the reference we are calling from:

// These calls with default parameters...
subTag.WriteTag();
subByBaseTag.WriteTag();
subByInterfaceTag.WriteTag();

// Are expaneded to these by the compiler...
subTag.WriteTag("SubTag");
subByBaseTag.WriteTag("BaseTag");
subByInterfaceTag.WriteTag("ITag");

That is, when we called WriteTag() from subByBaseTag, we used the default parameter defined in BaseTag since that was the type of the reference subByBaseTag, and so on.

Further, if you define the defaults in a base class (or interface), because they are compile-time magic only, they are not inherited!  They only apply for references of that type.  Thus, if you have the following definition:

public interface IAnotherTag {
  void WriteTag(string tagName = "I have a default!");
}

public class SomeTag : IAnotherTag {
  public virtual void WriteTag(string tagName) {
    Console.WriteLine(tagName);
  }
}

And have the following lines in main, the first attempt to use the default succeeds, while the second fails with a compile-time error!

public static void Main() {
  SomeTag tag = new SomeTag();
  IAnotherTag tagByInterface = tag;

  // this works because the default was defined in the interface
  tagByInterface.WriteTag();

  // this will not compile because SomeTag doesn't have a default defined!
  tag.WriteTag();
}

So remember the rule: default parameters substitutions are determined at compile-timenot at run-time.  You will get the default value defined (if any) from the reference type you are calling from.

Summary

Default parameters are a great tool for reducing the number of redundant overloads of methods and constructors.  They have many benefits, but can bite you if you forget that substitutions are determined at compile-time and not run-time. 

As such, you may want to limit to defining them in sealed classes

Technorati Tags: C#CSharp.NETSoftwareDefault ParametersLittle Pitfalls

This article is part of the GWB Archives. Original Author: James Michael Hare

Related Posts