James Michael Hare

...hare-brained ideas from the realm of software development...
posts - 136 , comments - 1076 , trackbacks - 0

My Links

News

Welcome to my blog! I'm a Sr. Software Development Engineer in Seattle, WA. I've been doing C++/C#/Java development for over 18 years, but have definitely learned that there is always more to learn!

All thoughts and opinions expressed in my blog and my comments are my own and do not represent the thoughts of my employer.

Blogs I Read

MCC Logo MVP Logo

Follow BlkRabbitCoder on Twitter

Tag Cloud

Archives

Post Categories

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:

   1: public class Message
   2: {
   3:     // can either cascade these like this or duplicate the defaults (which can introduce risk)
   4:     public Message() : this(string.Empty)
   5:     {                
   6:     } 
   7:  
   8:     public Message(string text) : this(text, null)
   9:     {                
  10:     } 
  11:  
  12:     public Message(string text, IDictionary<string, string> properties)
  13:         : this(text, properties, -1)
  14:     {                
  15:     } 
  16:  
  17:     public Message(string text, IDictionary<string, string> properties, long timeToLive)   
  18:     {          
  19:         // ...
  20:     }
  21: } 

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

   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: } 

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:

   1: 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:

   1: 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:

   1: public class MyClass
   2: {
   3:     public static void SomeMethod(int x = 5, int y = 7)
   4:     {
   5:         Console.WriteLine("Called with: {0}, {1}", x, y);
   6:     }
   7:  
   8:     public static void Main()
   9:     {
  10:         SomeMethod(13);
  11:     }
  12: }

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:

   1: // the default parameters are substituted at compile-time!
   2: SomeMethod(13);
   3: 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!

   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: } 

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:

   1: public static void Main() 
   2: {
   3:     // three different typed references to same object instance
   4:     SubTag subTag = new SubTag();
   5:     BaseTag subByBaseTag = subTag;
   6:     ITag subByInterfaceTag = subTag; 
   7:  
   8:     // what happens here?
   9:     subTag.WriteTag();       
  10:     subByBaseTag.WriteTag(); 
  11:     subByInterfaceTag.WriteTag(); 
  12: }

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:

   1: SubTag
   2: BaseTag
   3: 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:

   1: // These calls with default parameters...
   2: subTag.WriteTag();       
   3: subByBaseTag.WriteTag(); 
   4: subByInterfaceTag.WriteTag(); 
   5:  
   6: // Are expaneded to these by the compiler...
   7: subTag.WriteTag("SubTag");           
   8: subByBaseTag.WriteTag("BaseTag");    
   9: 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:

   1: public interface IAnotherTag
   2: {
   3:     void WriteTag(string tagName = "I have a default!");
   4: }
   5:  
   6: public class SomeTag : IAnotherTag
   7: {
   8:     public virtual void WriteTag(string tagName)
   9:     {
  10:         Console.WriteLine(tagName);
  11:     }
  12: }

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

   1: public static void Main()
   2: {
   3:     SomeTag tag = new SomeTag();
   4:     IAnotherTag tagByInterface = tag;
   5:  
   6:     // this works because the default was defined in the interface
   7:     tagByInterface.WriteTag(); 
   8:  
   9:     // this will not compile because SomeTag doesn't have a default defined!
  10:     tag.WriteTag();
  11: }

So remember the rule: default parameters substitutions are determined at compile-time, not 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 at the end of hierarchy chains.  Avoid defining them in interfaces or base classes, as this may lead to confusion if they are not implemented in sub-classes – or worse, are implemented with different values!

  Technorati Tags: , , , , ,

 

 

Print | posted on Thursday, July 28, 2011 6:24 PM | Filed Under [ My Blog C# Software .NET Little Pitfalls ]

Feedback

Gravatar

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

Excellent article! It's a damned shame that the default params doesn't work with DateTime.
7/29/2011 8:00 AM | Dan Atkinson
Gravatar

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

@Dan: Yep! Unfortunately once again the key is default parameters must be compile-time constants, which unfortunately means they have to be things that can only be resolved at compile time (literals, null, etc).

The only real workaround there is overloads that would pass in the date you want to a more explicit overload.
7/29/2011 8:49 AM | James Michael Hare
Gravatar

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

In this article, you said
1: // These calls with default parameters...
2: subTag.WriteTag();
3: subByBaseTag.WriteTag();
4: subByInterfaceTag.WriteTag();
5:

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

In your previous article you said.
6: myB.WhoAmI(); // I am a B
7: myBasA.WhoAmI(); // I am a B
Note that in the code above, even though reference myBasA is typed as A, the actual object being referred to is of type B, thus B.WhoAmI() will be called at runtime.

As per this article doesn't the compiler do the following.
6: // Are expaneded to these by the compiler...
7: subTag.WriteTag("SubTag");
8: subByBaseTag.WriteTag("SubTag");
9: subByInterfaceTag.WriteTag("SubTag");
Please clarify
7/30/2011 8:50 PM | NM
Gravatar

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

@NM:

No, the compiler will do:

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

Remember, the mechanism here behind the scenes is a compile-time substitution. Because subTag is a reference of type SubTag, and subByBasetag is a reference of type BaseTag, and subByInterfaceTag is a reference of type ITag.

They key here is understanding the difference between the type of reference and type of the object. For example:

ITag x = new SubTag();

In this line, the reference x is of type ITag, but the object refered to by x is of type SubTag.

Virtual methods (as shown in part of last week's post) evaluate based on the the type of the *object refered to* at runtime, which is why you'd get the subclass behavior on overrides.

Non-virtual methods (including hiding) is based on the compile time type which is the type of the *reference itself*.

Optional parameters also follow this model, and are compile-time substitutions and hence based on the *reference* type and not the runtime type of the object referred to.

Make sense?

-Jim
7/31/2011 1:07 AM | James Michael Hare
Gravatar

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

This is reallyy tricky. Once, while coding in VB6, I found optional parameters really useful when improving some methods. But VB6 isn't object oriented like C#, where an object is just a reference to a class.
8/2/2011 9:11 AM | yelinna
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 
 

Powered by: