Bart was a Visual C# MVP, now he works at Microsoft on the WPF dev team as Software Developer Engineer. Prior to this new challenge, Bart was active in the Belgian community on many Microsoft technologies, most of the time focusing on CLR, languages innovation and frameworks. In his role, he's been speaking at various events and attended several international conferences including TechEd Europe, IT Forum and the PDC. Bart is a DZone MVB and is not an employee of DZone and has posted 24 posts at DZone. You can read more from them at their website. View Full User Profile

C# 4.0 Feature Focus - Part 1 - Optional Parameters

11.06.2008
| 5713 views |
  • submit to reddit

Welcome to the first post in my new C# 4.0 Feature Focus series. Today we'll start by taking a look at optional parameters, a long-standing request from the community that made it to C# 4.0. By itself, the feature is definitely useful but in conjunction with the mission to make COM interop easier, there's even more value to it. In this post I'll outline what the feature looks like, how it's implemented and what the important caveats are.

The syntax

C# 4.0 can both declare and consume optional parameters. Here's a sample of a very simple method that declares a parameter as optional:

public static class OptionalDemoLib
{
public static void SayHello(string s = "Hello World!")
{
Console.WriteLine(s);
}
}

This means you can either call Do with one argument or without an argument, in which case the default value is used:

public static class OptionalDemo
{
public static void Main()
{
OptionalDemoLib.SayHello();
OptionalDemoLib.SayHello("Hello Bart!");
}
}

Notice all optional parameters need to come at the end of the argument list.

optlib.cs(3,58): error CS1737: Optional parameters must appear after all required parameters

If this weren't the case all sorts of ambiguities would result, e.g.:

public static void SayHello(string s1 = "Hello World!", string s2)

What would a call with a single string argument result in? Would the parameter be bound to s1, overriding the default, or would it bind to s2?

The implementation

How does it work? Let's start by taking a look at the definition side. Here's the IL corresponding to the declaration of SayHello above:

    .method public hidebysig static void  SayHello([opt] string s) cil managed
{
.param [1] = "Hello World!"
// Code size 9 (0x9)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call void [mscorlib]System.Console::WriteLine(string)
IL_0007: nop
IL_0008: ret
} // end of method OptionalDemoLib::SayHello

Two things are relevant here. First of all, the parameter is decorated with the [opt]. Second, the method body contains a .param directive. It turns out both of those primitives have been supported in the CLI since the very beginning. Visual Basic is one of the languages that already uses this today. Let's dive a little deeper using the CLI specification (ECMA 335), partition II:

  • 15.4    Defining methods

    opt specifies that this parameter is intended to be optional from an end-user point of view. The value to be supplied is stored using the .param syntax ($15.4.1.4).
  • 15.4.1    Method body

    | .param `[` Int32 `]` [ `=` FieldInit ]          Store a constant FieldInit value for parameter Int32.
  • 15.4.1.4    The .param directive

    This directive stores in the metadata a constant value associated with method parameter number Int32, see $22.9. (...) Unlike CIL instructions, .param uses index 0 to specify the return value of the method, index 1 to specify the first parameter of the method, ...
  • 22.9    Constant : 0x0B

    The Constant table is used to store compile-time, constant values for fields, parameters, and properties. The Constant table has the following columns:
    - Type ...
    - Parent ...
    - Value (an index into the Blob heap)

    Note that Constant information odes not directly influence runtime behavior, although it is visible via Reflection. Compilers inspect this information, at compile time, when importing metadata, but the value of the constant itself, if used, becomes embedded into the CIL stream the compiler emits. There are no CIL instructions to access the Constant table at runtime.

I'll come back to the remark in paragraph 22.9 in just a second. An important thing here is that the value needs to be constant, so no new'ing up of stuff or results of methods calls are allowed:

optlib.cs(3,23): error CS1736: Default parameter value for 's' must be a compile-time constant

What does the call-site look like if the parameter is omitted?

.method private hidebysig static void  Main() cil managed
{
.entrypoint
// Code size 24 (0x18)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Hello World!"
IL_0006: call void [optlib]OptionalDemoLib::Do(string)
IL_000b: nop
IL_000c: ldstr "Hello Bart!"
IL_0011: call void [optlib]OptionalDemoLib::Do(string)
IL_0016: nop
IL_0017: ret
} // end of method OptionalDemo::Main

Notice how remark 22.9 applies here. At the call-site both calls look like a call with one argument. The optional argument is "compiled away" on the side of the caller.

The caveat

The remark above quoting the CLI specification is a very important one:

Note that Constant information odes not directly influence runtime behavior, although it is visible via Reflection. Compilers inspect this information, at compile time, when importing metadata, but the value of the constant itself, if used, becomes embedded into the CIL stream the compiler emits. There are no CIL instructions to access the Constant table at runtime.

In human words, default values are burned into the call site. The metadata specified by the .param directive is only used to keep the constant value around, but as soon as a method is called and optional parameters are used in that call (as determined by the compiler), that value gets copied literally to the call site where it sticks. Let's illustrate this:

Step 1: Compile the following (csc /t:library optlib.cs)

using System;

public static class OptionalDemoLib
{
public static void SayHello(string s = "Hello World!")
{
Console.WriteLine(s);
}
}

Step 2: Compile the following (csc opt.cs /r:optlib.dll)

    using System;
using System.Reflection;

public static class OptionalDemo
{
public static void Main()
{
OptionalDemoLib.SayHello();
Console.WriteLine(typeof(OptionalDemoLib).GetMethod("SayHello").GetParameters()[0].RawDefaultValue);
}
}

Step 3: Run opt.exe

> opt.exe
Hello World!
Hello World!

Step 4: Change the library and recompile (don't recompile the opt.cs demo caller)

using System;

public static class OptionalDemoLib
{
public static void SayHello(string s = "Hello Universe!")
{
Console.WriteLine(s);
}
}

Step 5: Run opt.exe

> opt.exe
Hello World!
Hello Universe!

In step 2 we're introducing the use of reflection to get the default value for the optional parameter. This is run-time reflection that actually inspects the metadata associated with the method's parameter. However, as 22.9 mentions: "There are no CIL instructions to access the Constant table at runtime.", so the default value gets burned into the call site by the compiler. This is no different than the same constant-ness encountered in the difference between readonly variables and constants.

The key take-away from this: once you expose a default parameter value on a public method, you can never change it without recompiling all clients that depend on it. For library writers, this never means never ever. If you need the flexibility of changing defaults afterwards, consider providing overloads instead:

using System;

public static class OptionalDemoLib
{
public static void SayHello()
{
SayHello("Hello Universe!");
}

public static void SayHello(string s)
{
Console.WriteLine(s);
}
}

This way the constant remains on the definition side and can be changed over there at will. Not that you should do so regularly of course, as you're after all changing defaults that are hopefully documented somewhere in the XML comments for your public methods. Yet another way to attack the problem if you have a bunch of parameters is to take in a property "bag" as the argument to the method (in practice and object with properties for all the supported setting "parameters"). That way every value can be optional and the method can examine whether certain omissions can be granted. Dispatching to the internal implementation with the maximum parameter list could use techniques like null-coalescing (??):

public static void ComplexSayHello(Message arg)
{
ComplexSayHelloInternal(..., arg.Text ?? "Hello Universe!", ...);
}

Use the right approach - all techniques have their benefits.

The past

We've talked about the future, but let me point out it was actually possible to declare optional parameters in C# before, using parameter metadata custom attributes:

    using System;
using System.Runtime.InteropServices;

public static class OptionalDemoLib
{
public static void SayHello([Optional][DefaultParameterValue("Hello Universe!")] string s)
{
Console.WriteLine(s);
}
}

This produces the same IL as the sample shown earlier using C# 4.0 syntax. C# couldn't consume this method though without specifying all parameters, but now it can.

Conclusion

Optional parameters are a beautiful feature and will make life easier when dealing with old COM-driven libraries that were designed with this language feature in mind (amongst others such as named parameters, see next post). To keep the picture symmetric, C# 4.0 also provides the ability to define optional parameters, but be well-aware of the call site burning fact mentioned above. Only use optional parameters if the optional value is really constant in the time dimension unless you're never going to expose the optional value to the outside world (but it's damn easy to make a previously internal or private method public and forgetting about this fact, so the first rule should be the strongest decisive factor...). Don't get me wrong: I like the feature a lot, but powerful weapons need safety warnings.

Next time: named parameters. Enjoy!

References
Published at DZone with permission of Bart De Smet, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Mark Kamoski replied on Mon, 2008/11/17 - 2:25pm

Named parameters inline. Ug. That is a direct steal from classic VB, the oft maligned, seldom understood, GREAT (yes I said GREAT) language. Glad to see it. Late by years (maybe decades?).

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.