c# Optional Parameters - WAT?

Optional parameters have been around since c# 4.0, but I learnt a few things about them the other day which I found interesting. After running by what I'd seen with a number of other experienced developers, they too were surprised about the results.

After some digging, I came across an Stackoverflow post- Eric Lippert Discusses on SO where a number of developers were discussing these scenarios with Eric Lippert, where even he conceded that some of these oddities where among the reasons that optional parameters were not implemented in c# sooner. As such, I thought I'd write up a post to cover some of the things I discovered.

The Oddities

Given the following interface and implementation:

public interface ICustomer
{
    string DoSomething(string name = "interface default value");
}

public class Customer : ICustomer
{
    public string DoSomething(string name = "implementation default value")
    {
        return name;
    }
}

What would you expect the value of result to be?

   static void Main(string[] args)
    {           
        Customer customer = new Customer();
        var result = MyMethod(customer);
        // What is the value of result?
    }

    static string MyMethod(ICustomer customer)
    {
        return customer.DoSomething();
    }

Go on....come up with an answer!

If you said "implementation default value" then wouldn't be alone. In my highly scientific test of asking 4 developers who have been working with c# for over 10 years each, they all assumed this answer too. I mean, it makes sense right? You're instantiating a concrete class, and passing this to a method, you'd assume the definition of that class would be respected. But you'd be wrong. To understand why, lets look at the IL generated when compiling the above, MyMethod method:

.method private hidebysig static string  MyMethod(class DefaultParameters.ICustomer customer) cil managed
{
  // Code size       17 (0x11)
  .maxstack  2
  .locals init ([0] string V_0)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldstr      "interface default value"
  IL_0007:  callvirt   instance string DefaultParameters.ICustomer::DoSomething(string)
  IL_000c:  stloc.0
  IL_000d:  br.s       IL_000f
  IL_000f:  ldloc.0
  IL_0010:  ret
} // end of method Program::MyMethod

Looking at the following line IL_0002: ldstr "interface default value" we can see that the IL is loading the compile time constant of "interface default value" onto the evaluation stack before the callvirt instruction invokes the MyMethod method. This means the value of the name parameter is assigned at compile time, not run time. Knowing this, should help clarify the result we're seeing: at compile time the signature of the method is used to generate the IL that will form our executable. This makes sense when you consider that another implementation of ICustomer may be passed to MyMethod from another consumer of the method, public class Customer2 : ICustomer for example.

Ok, so it logically makes sense, even though it may be counter-intuitive. Things get even more counter-intuitive in the following example:

    static void Main(string[] args)
    {
        ICustomer customer = new Customer();
        var result = customer.DoSomething();
        // What is the value of result?

        Customer customer2 = new Customer();
        var result2 = customer2.DoSomething();
        // What is the value of result2?

        var customer3 = new Customer();
        var result3 = customer3.DoSomething();
        // What is the value of result3?

        var customers = new[] {customer, customer2, customer3};
        foreach (var c in customers)
        {
            var result4 = c.DoSomething();
            // What is the value of result4 on each interation of the foreach loop.
        }
    }

    static string MyMethod(ICustomer customer)
    {
        return customer.DoSomething();
    }

The following results are returned with the above.

Result = "Interface default value"
Result2 = "Implementation default value"
Result3 = "Implementation default value"
Result4 (all interations) = "Interface default value"

After the previous look at the IL, I think the reason we get the results above is pretty obvious, it is however a source of subtle and difficult to spot bugs. Intuitively the result from 1,2 and 3 should be the same. Even more counter-intuitive, is that passing those references to a method and invoking the same method produces a different result to invoking it directly.

The above scenario reminds me a lot of the situation you get with inadvertently hiding a virtual method from a base class whereby you get a different method being invoked depending on whether the type holding the reference is is that of the base class or sub class. The CS compiler is kind enough to give us a warning CS0114 in this situation, but no such luck when it comes to optional parameters!

One more scenario that is worth mentioning is that the follow interface and class definition are both completely valid:

public interface ICustomer
{
    string DoSomething(string name = "interface default value");
}

public class Customer : ICustomer
{
    public string DoSomething(string name)
    {
        return name;
    }
}

This results in a situation that feels like a violation of Liskov Substition Principle:

    static void Main(string[] args)
    {
        ICustomer customer = new Customer();
        var result = customer.DoSomething();
        // What is the value of result?

        Customer customer2 = new Customer();
        **// Will not compile - CS7036 **
        var result2 = customer2.DoSomething();                       
    }

Conclusion

Optional parameters can sometimes lead to surprises and should be used wisely!

It's important to remember that primary use case for the introduction of optional parameters was inter-op with legacy code that supported them.

MSDN:

Some APIs, most notably COM interfaces such as the Office automation APIs, are written specifically with named and optional parameters in mind. Up until now it has been very painful to call into these APIs from C#, with sometimes as many as thirty arguments having to be explicitly passed, most of which have reasonable default values and could be omitted.

They are commonly used however as a shortcut for creating a method overload and when the introduction of a non-optional parameter would have a high refactoring cost. Both of which, whilst pragmatically OK if real life/business gets in the way of tidy code are OK, should be viewed as shortcuts/hacks and should be avoided if at all possible.

Further Reading

Sam Shiles

Read more posts by this author.