C# Corner

Looking Ahead to C# 4.0: Optional and Named Parameters

Optional and named parameters were added to the C# language for COM and Office interoperability, but these features are actually useful in a variety of ways.

This month I'm going to explore two related upcoming features in the C# language: named and optional parameters. These features were added to the language to support COM interoperability, specifically COM interoperability with Microsoft Office.

For a variety of historical reasons, Office COM APIs have large numbers of parameters, several of which have reasonable defaults. In addition, while you may want to use many of the defaults, you may need to specify a value for some of the parameters later in the list. It would be great to call those Office methods having 15 or more parameters by only specifying those parameters where you want something other than the default. Optional and named parameters enable that for you in C# 4.0.

However, like all language features, once created you'll undoubtedly uncover other uses for optional and named parameters in your own code. Callers that use your classes will want to make use of the optional and named parameters in your APIs as well.

Keep in mind that you'll need to make a few changes in your regular coding practices in order to take advantage of these new features, and to avoid making mistakes that will ripple into your users' code.

Named and Optional Parameters Explained
Named parameters allow you to call a method by specifying which argument in a method call refers to which formal parameter. Suppose someone had written this trivial Subtract method:

public static int Subtract(int left, int right)
{
  return left - right;
}

All three of these calls are equivalent:

result = Subtract(7, 5);
result = Subtract(left: 7, right: 5);
result = Subtract(right: 5, left: 7);

If you don't specify a name for any of the parameters, the normal order is used. Notice that you can rearrange the order of the parameters by specifying them by name.

Optional parameters enable you to specify a default value for any parameter to a method. Any parameters that have default values must be at the end of the argument list. Optional parameters must be compile-time constants. That means you can't create new objects, or use static variables or method returns as the default parameter value. You specify a default value for an optional parameter by declaring its value in the method definition. For example, you could update the Subtract method to have a default behavior of subtracting 0:

public static int Subtract(int left, int right = 0)
{
  return left - right;
}

Optional and named parameters work together to provide a richer calling sequence. Callers can specify none, any or all of the optional parameters by name.

This isn't a particularly useful example, but its simplicity does make it easy to see the syntax. Now, let's dive into some real-world examples and show where these features can be useful in a host of different coding tasks.

Optional Parameters Instead of Overloads
You've undoubtedly seen that many of the methods in .NET Framework have multiple overloads. That means someone had to create those multiple overloads. In practice, many of those overloads will make use of a common implementation. But even so, many different overloads can introduce subtle bugs in your code base.

Instead, when you find yourself creating multiple overloads for a method, consider creating one method with optional parameters. That saves you quite a bit of work, because you need to create only one method to enable users to specify only those parameters where the default is not what is being requested.

public enum Priority
{
  NeverGetsDone,
  Low,
  Medium,
  High,
  FightThisFireNow
}
public class ProjectDescription
{
  public string Name { get; private set; }
  public DateTime DueDate { get; private set; }
  public string Customer { get; private set; }
  public string Description { get; private set; }
  public Priority Importance { get; private set; }

  public ProjectDescription(string name,
    DateTime dueDate, 
    string customer = null,
    string description = null, 
    Priority p = Priority.Medium)
  {
    Name = name;
    DueDate = dueDate;
    Customer = customer;
    Description = (description == null) ? 
      string.Empty : description;
    Importance = p;
  }

  public void AddDescription(string newInformation)
  {
    Description += newInformation;
  }
}

The code above shows a portion of a ProjectDescription class. In this design, some of the properties shown are meant to be immutable; they should only be set by the constructor. However, sometimes you may just want some defaults. An internal project would have a null customer name. Often, you don't know the priority, so you just guess. Finally, the description can be added later, as more information becomes available on the project.

Notice a number of the constructor parameters have default values. Without those defaults, you'd have to create many more overloads of the constructor. You can specify just the project name and due date:

ProjectDescription proj1 = new ProjectDescription(
  "Sample Proj",
   DateTime.Now + TimeSpan.FromDays(3));

You can also skip some of the optional parameters and specify priority without a description or a customer:

ProjectDescription proj2 = new ProjectDescription(
  "FIREDRILL", DateTime.Now,
  p : Priority.FightThisFireNow);

Providing optional parameters makes it easier to provide what appears to be a rich set of overloads, while still only defining a single method. Even better, if some later enhancement requires new parameters, you can use default values to minimize the disruption to your callers.

Of course, Optional Parameters introduce behavior that can lead to problems if you don't understand how the feature is implemented. Optional parameters are added at the call site by the compiler.

That means when you write this:

ProjectDescription proj1 = new ProjectDescription(
  "Sample Proj", 
  DateTime.Now + TimeSpan.FromDays(3));

The compiler generates this:

ProjectDescription proj1Prime = new 
ProjectDescription(
  "Sample Proj", 
  DateTime.Now + TimeSpan.FromDays(3),
  null, null, Priority.Medium);

The compiler creates all parameters you omitted and gives them their default values. It's important to remember that those values are added at the compile time when the calling code is compiled. Suppose that, later, the manager decides all new projects should be high priority. You make the change to the constructor definition:

public ProjectDescription(string name, 
DateTime dueDate, 
  string customer = null,
   string description = null, 
Priority p = Priority.High)
{
  Name = name;
  DueDate = dueDate;
  Customer = customer;
  Description = (description == null) ? 
    string.Empty : description;
  Importance = p;
}

That seems simple enough-but it's a form of breaking change. You need to rebuild every assembly that creates a new ProjectDescription. You can't patch a working system by dropping in a new version of the assembly containing the ProjectDescription class. All the callers that used the previous default value of Priority.Medium would still use that value. The value of a default parameter is baked into the calling code at compile time; later changes to the called method require a rebuild of all calling components.

In addition, optional parameters can confuse the already complicated method overload resolution rules in C#. Suppose that someone later adds a second constructor to ProjectDescription where all the optional parameters are omitted:

// Subtle bug, different default behavior
public ProjectDescription(string name, 
DateTime dueDate)
{
  Name = name;
  DueDate = dueDate;
  Customer = string.Empty;
  Description = string.Empty;
  Importance = Priority.Low;
}

That's perfectly legal C#. Now, this call would go to this new constructor:

ProjectDescription proj1 = new ProjectDescription(
  "Sample Proj", 
  DateTime.Now + TimeSpan.FromDays(3));

If any of the optional parameters are specified, the original constructor would be called. I changed the default behavior and introduced a subtle bug so that it would be easy to see that the compiler does generate calls to both the constructors based on the number of the parameters. An exact match goes to the constructor with two parameters; all other calls go the constructor with all parameters and the optional values.

As with changing the default values, adding an overloaded method would require a full rebuild of all calling assemblies in order to have those changes reflected at call sites.

Named Parameters Can Improve Readability
One of the things I like the most about named parameters is that you can improve the readability of code. For example, examine these two person declarations:

Person author = new Person(
"Bill", "Wagner");
Person editor = new Person(
"Desmond", "Michael");

One of them is wrong. However, it's impossible to tell which one is wrong from the code snippets above. Whenever you have a method that contains multiple parameters of the same type, explicitly naming the parameters can greatly improve readability:

Person author = new Person(
  firstName : "Bill",
  lastName : "Wagner");
Person editor = new Person(
  lastName: "Desmond", 
  firstName : "Michael");

Naming parameters in the calling code removes any possible ambiguity among parameters that have the same type. Think through all the methods you call that have multiple parameters of the same type: x,y coordinates in points, methods with multiple string parameters and so on. Explicitly naming those parameters at the call site will minimize confusion and errors.

Of course, named parameters also can introduce problems in your code base. The most obvious is that renaming a formal parameter must be considered a breaking change. The Visual Studio IDE does correctly rename the parameter at all call sites, so inside a single solution, this isn't a problem. You need to be extremely careful about renaming formal parameters to public methods if you deliver assemblies to customers as part of a class library. Formal parameter name changes will break their code.

There are also subtle effects that can occur because the parameters to a method are evaluated in the order of their appearance. To demonstrate this I'll use a simple method to generate numbers in order:

static int initial = 0;
private static int GetNextNumber()
{
  return initial++;
}

This method returns the values 0, 1, and so on, in sequence each time it's called. Therefore, this call should always result in the value -1:

int value = Subtract(GetNextNumber(), 
GetNextNumber());

However, that's not necessarily the case. By switching the parameters, you can change the order of evaluation and have Subtract() return 1:

int result = Subtract(
right: GetNextNumber(), 
  left: GetNextNumber());

The parameters are evaluated in the order they appear, even if they appear in a different order in the formal parameter list to the method. That last call evaluates the right argument before the left. Therefore, the right argument is less than the left argument.

Named and optional parameters were added to the C# language in order to provide better support for COM interoperability, and especially the Office APIs. However, don't limit your use of these features to those days when you're working with COM and Office. The C# team thought hard about creating a feature set that makes COM interoperability easier and yet has general usefulness outside of those scenarios. Like every language feature, you'll need to invest some time in understanding the subtle behaviors of the features in order to avoid the pitfalls. That's time well spent to add more capabilities to your skill set.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube