C# .NET Optional Parameters and Named Arguments

Jon Skeet explains optional parameters, whose values don’t have to be explicitly specified by the caller. He also talks about named arguments. The idea of named arguments is that, when you specify an argument value, you can also specify the name of the parameter it’s supplying the value for.

This article is taken from the book C# in Depth, Second Edition., by Jon Skeet. It is being reproduced here by permission from Manning Publications. Manning Early Access Program (MEAP) books and ebooks are sold exclusively through Manning. Visit the book's page for more information.  November 2010 | 584 pages  ISBN: 9781935182474

You can get an instant 40% discount on this MEAP edition by simply clicking on the link above, and use promo code egghead40

Optional parameters and named arguments are perhaps the Batman and Robin[1] features of C# 4. They’re distinct but usually seen together. I’m going to keep them apart for the moment so we can examine each in turn, but then we’ll use them together for some more interesting examples.

PARAMETERS AND ARGUMENTS This article obviously talks about parameters and arguments a lot. In casual conversation, the two terms are often used interchangeably, but I’m going to use them in line with their formal definitions. A parameter (also known as a formal parameter) is the variable that’s part of the method or indexer declaration. An argument is an expression used when calling the method or indexer. So, for example, consider this snippet:

void Foo(int x, int y)

{

// Do something with x and y

}

...

int a = 10;

Foo(a, 20);

Here the parameters are x and y, and the arguments are a and 20.

We’ll start by looking at optional parameters.

Optional parameters

Visual Basic has had optional parameters for ages, and they’ve been in the CLR from .NET 1.0. The concept is as obvious as it sounds: some parameters are optional, so their values don’t have to be explicitly specified by the caller. Any parameter that hasn’t been specified as an argument by the caller is given a default value.

Motivation

Optional parameters are usually used when there are several values required for an operation, where the same values are used a lot of the time. For example, suppose you wanted to read a text file; you might want to provide a method that allows the caller to specify the name of the file and the encoding to use. The encoding is almost always UTF-8, though, so it’s nice to be able to use that automatically if it’s all you need.

Historically the idiomatic way of allowing this in C# has been to use method overloading: declare one method with all the possible parameters, and others that call that method, passing in default values where appropriate. For instance, you might create methods like this:

public IList<Customer> LoadCustomers(string filename,

Encoding encoding)

{

... #A

}

public IList<Customer> LoadCustomers(string filename)

{

return LoadCustomers(filename, Encoding.UTF8); #B

}

#A Do real work here

#B Default to UTF-8

This works fine for a single parameter, but it becomes trickier when there are multiple options. Each extra option doubles the number of possible overloads, and if two of them are of the same type, you can have problems due to trying to declare multiple methods with the same signature. Often the same set of overloads is also required for multiple parameter types. For example, the XmlReader.Create() method can create an XmlReader from a Stream, a TextReader, or a string—but it also provides the option of specifying an XmlReaderSettings and other arguments. Due to this duplication, there are 12 overloads for the method. This could be significantly reduced with optional parameters. Let’s see how it’s done.

Declaring optional parameters and omitting them when supplying arguments

Making a parameter optional is as simple as supplying a default value for it, using what looks like a variable initializer. Figure 1 shows a method with three parameters: two are optional, one is required.

Figure 1 Declaring optional parameters

All the method does is print out the arguments, but that’s enough to see what's going on. The following listing gives the full code and calls the method three times, specifying a different number of arguments for each call.

Listing 1 Declaring a method with optional parameters and calling

static void Dump(int x, int y = 20, int z = 30) #1

{

Console.WriteLine("x={0} y={1} z={2}", x, y, z);

}

...

Dump(1, 2, 3); #2

Dump(1, 2); #3

Dump(1); #4

#1 Declares method with optional parameters

#2 Calls method with all arguments

#3 Omits one argument

#4 Omits two arguments

The optional parameters are the ones with default values specified (#1). If the caller doesn’t specify y, its initial value will be 20, and likewise z has a default value of 30. The first call (#2) explicitly specifies all the arguments; the remaining calls (#2 and #4) omit one or two arguments respectively, so the default values are used. When there’s one argument missing, the compiler assumes that the final parameter has been omitted—then the penultimate one, and so on. The output is therefore

x=1 y=2 z=3

x=1 y=2 z=30

x=1 y=20 z=30

Note that although the compiler could use some clever analysis of the types of the optional parameters and the arguments to work out what’s been left out, it doesn’t: it assumes that you’re supplying arguments in the same order as the parameters[2]. This means that the following code is invalid:

static void TwoOptionalParameters(int x = 10,

string y = "default")

{

Console.WriteLine("x={0} y={1}", x, y);

}

...

TwoOptionalParameters("second parameter"); #A

#A Error!

This tries to call the TwoOptionalParameters method specifying a string for the first argument. There’s no overload with a first parameter that’s convertible from a string, so the compiler issues an error. This is a good thing—overload resolution is tricky enough (particularly when generic type inference gets involved) without the compiler trying all kinds of different permutations to find something you might be trying to call.

If you want to omit the value for one optional parameter but specify a later one, you need to use named arguments.

Restrictions on optional parameters

There are a few rules for optional parameters. All optional parameters must come after required parameters. The exception to this is a parameter array (as declared with the params modifier), which still has to come at the end of a parameter list, but can come after optional parameters. A parameter array can’t be declared as an optional parameter—if the caller doesn’t specify any values for it, an empty array will be used instead. Optional parameters can’t have ref or out modifiers either.

An optional parameter can be of any type, but there are restrictions on the default value specified. You can always use constants: numeric and string literals, null, const members, enum members, and the default(T) operator. Additionally, for value types, you can call the parameterless constructor, although this is equivalent to using the default (...) operator anyway. There has to be an implicit conversion from the specified value to the parameter type, but this must not be a user-defined conversion.

Table 1 shows some examples of valid parameter lists.

Table 1 Valid method parameter lists using optional parameters

Declaration

Notes

Foo(int x, int y = 10)

Numeric literal used for default value

Foo(decimal x = 10)

Implicit built-in conversion from int to decimal

Foo(string name = "default")

String literal used for default value

Foo(DateTime dt = new DateTime())

Zero value of DateTime

Foo(DateTime dt = default(DateTime))

Alternative syntax for the zero value

Foo<T>(T value = default(T))

Default value operator works with type parameters

Foo(int? x = null)

Nullable conversion

Foo(int x, int y = 10, params int[] z)

Parameter array after optional parameters

By contrast, table 2 shows some invalid parameter lists and explains why they’re not allowed.

Table 2 Invalid method parameter lists using optional parameters

Declaration (invalid)

Notes

Foo(int x = 0, int y)

Required non-params parameter can’t come after an optional parameter

Foo(DateTime dt = DateTime.Now)

Default values must be constants

Foo(XName name = "default")

Conversion from string to XName is user defined

Foo(params string[] names = null)

Parameter arrays can’t be optional

Foo(ref string name = "default")

ref/out parameters can’t be optional

The fact that the default value has to be constant is a pain in two different ways. One of them is familiar from a slightly different context, as we’ll see now.

Versioning and optional parameters

The restrictions on default values for optional parameters may remind you of the restrictions on const fields or attribute values, and they behave very similarly. In both cases, when the compiler references the value, it copies it directly into the output. The generated IL acts exactly as if your original source code had contained the default value. This means if you ever change the default value without recompiling everything that references it, the old callers will still be using the old default value. To make this concrete, imagine this set of steps:

1. Create a class library (Library.dll) with a class like this:

public class LibraryDemo

{

public static void PrintValue(int value = 10)

{

System.Console.WriteLine(value);

}

}

2. Create a console application (Application.exe) that references the class library:

public class Program

{

static void Main()

{

LibraryDemo.PrintValue();

}

}

3. Run the application—it’ll print 10, predictably.

4. Change the declaration of PrintValue as follows, then recompile just the class library:

public static void PrintValue(int value = 20)

5. Rerun the application—it’ll still print 10. The value has been compiled directly into the executable.

6. Recompile the application and rerun it—this time it’ll print 20.

This versioning issue can cause bugs that are hard to track down, because all the code looks correct. Essentially, you’re restricted to using genuine constants that should never change as default values for optional parameters[3]. There’s one benefit of this setup: it gives the caller a guarantee that the value it knew about at compile-time is the one that’ll be used. Developers may feel more comfortable with that than with a dynamically computed value, or one that depends on the version of the library used at execution time.

Of course, this also means you can’t use any values that can’t be expressed as constants anyway—you can’t create a method with a default value of “the current time,” for example.

Making defaults more flexible with nullity

Fortunately, there’s a way round this. Essentially you introduce a magic value to represent the default, and then replace that magic value with the real default within the method itself. If the phrase magic value bothers you, I’m not surprised—except we’re going to use null for the magic value, which already represents the absence of a normal value. If the parameter type would normally be a value type, we simply make it the corresponding nullable value type, at which point we can still specify that the default value is null.

As an example of this, let’s look at a similar situation to the one I used to introduce the whole topic: allowing the caller to supply an appropriate text encoding to a method, but defaulting to UTF-8. We can’t specify the default encoding as Encoding. UTF8 as that’s not a constant value, but we can treat a null parameter value as “use the default.” To demonstrate how we can handle value types, we’ll make the method append a timestamp to a text file with a message. We’ll default the encoding to UTF-8 and the timestamp to the current time. Listing 2 shows the complete code and a few examples of using it.

Listing 2 Using null default values to handle nonconstant situations

static void AppendTimestamp(string filename, #A

string message,

Encoding encoding = null, #1

DateTime? timestamp = null)

{

Encoding realEncoding = encoding ?? Encoding.UTF8; #2

DateTime realTimestamp = timestamp ?? DateTime.Now;

using (TextWriter writer = new StreamWriter(filename,

true,

realEncoding))

{

writer.WriteLine("{0:s}: {1}", realTimestamp, message);

}

}

...

AppendTimestamp("utf8.txt", "First message");

AppendTimestamp("ascii.txt", "ASCII", Encoding.ASCII);

AppendTimestamp("utf8.txt", "Message in the future", null, #3

new DateTime(2030, 1, 1));

#A Two required parameters

#1 Two optional parameters

#2 Null coalescing operator for convenience

#3 Explicit use of null

Listing 2 shows a few nice features of this approach. First, we’ve solved the versioning problem. The default values for the optional parameters are null (#1), but the effective values are “the UTF-8 encoding” and “the current date and time.” Neither of these could be expressed as constants, and should we ever wish to change the effective default—for example to use the current UTC time instead of the local time—we could do so without having to recompile everything that called AppendTimestamp. Of course, changing the effective default changes the behavior of the method—you need to take the same sort of care over this as you would with any other code change. At this point, you (as the library author) are in charge of the versioning story—you’re taking responsibility for not breaking clients, effectively. At least it’s more familiar territory: you know that all callers will experience the same behavior, regardless of recompilation.

We’ve also introduced an extra level of flexibility. Not only do optional parameters mean we can make the calls shorter, but having a specific “use the default” value means that should we ever wish to, we can explicitly make a call allowing the method to choose the appropriate value. At the moment, this is the only way we know to specify the timestamp explicitly without also providing an encoding (#3), but that’ll change when we look at named arguments.

The optional parameter values are simple to deal with thanks to the null coalescing operator (#2). I’ve used separate variables for the sake of printed formatting, but in real code you’d probably use the same expressions directly in the calls to the Stream-Writer constructor and the WriteLine method.

There are two downsides to this approach: first, it means that if a caller accidentally passes in null due to a bug, it’ll get the default value instead of an exception. In cases where you’re using a nullable value type and callers will either explicitly use null or have a non-nullable argument, that’s not much of a problem—but for reference types it could be an issue.

On a related note, it requires that you don’t want to use null as a “real” value[4]. There are occasions where you want null to mean null—and if you don’t want that to be the default value, you’ll have to find a different constant or just leave the parameter as a required one. But in other cases where there isn’t an obvious constant value that’ll clearly always be the right default, I’d recommend this approach to optional parameters as one that’s easy to follow consistently and removes some of the normal difficulties.

We’ll need to look at how optional parameters affect overload resolution, but it makes sense to wait until we’ve seen how named arguments work. Speaking of which...

Named arguments

The basic idea of named arguments is that when you specify an argument value, you can also specify the name of the parameter it’s supplying the value for. The compiler then makes sure that there is a parameter of the right name, and uses the value for that parameter. Even on its own, this can increase readability in some cases. In reality, named arguments are most useful in cases where optional parameters are also likely to appear, but we’ll look at the simple situation first.

INDEXERS, OPTIONAL PARAMETERS, AND NAMED ARGUMENTS You can use optional parameters and named arguments with indexers as well as methods. But this is only useful for indexers with more than one parameter: you can’t access an indexer without specifying at least one argument anyway. Given this limitation, I don’t expect to see the feature used much with indexers. It works exactly as you’d expect it to, though.

I’m sure we’ve all seen code that looks something like this:

MessageBox.Show("Please do not press this button again", // text

"Ouch!"); // title

I’ve actually chosen a pretty tame example; it can get a lot worse when there are loads of arguments, especially if a lot of them are the same type. But this is still realistic: even with just two parameters, I’d find myself guessing which argument meant what based on the text when reading this code, unless it had comments like the ones I have here. There’s a problem though: comments can lie about the code they describe. Nothing is checking them at all. Named arguments ask the compiler to help.

Syntax

All we need to do to the previous example is prefix each argument with the name of the corresponding parameter and a colon:

MessageBox.Show(text: "Please do not press this button again",

caption: "Ouch!");

Admittedly we now don’t get to choose the name we find most meaningful (I prefer title to caption) but at least we’ll know if we get something wrong. Of course, the most common way in which we could get something wrong here is to get the arguments the wrong way around. Without named arguments, this would be a problem: we’d end up with the pieces of text switched in the message box. With named arguments, the ordering becomes largely irrelevant. We can rewrite the previous code like this:

MessageBox.Show(caption: "Ouch!",

text: "Please do not press this button again");

We’d still have the right text in the right place, because the compiler would work out what we meant based on the names. For another example, look at the StreamWriter constructor call we used in listing 2. The second argument is just true—what does this mean? Is it going to force a stream flush after every write? Include a byte order mark? Append to an existing file instead of creating a new one? Here’s the equivalent call using named arguments:

new StreamWriter(path: filename,

append: true,

encoding: realEncoding)

In both of the examples, we’ve seen how named arguments effectively attach semantic meaning to values. In the never-ending quest to make our code communicate better with humans as well as computers, this is a definite step forward. I’m not suggesting that named arguments should be used when the meaning is already obvious, of course. Like all features, it should be used with discretion and thought.

NAMED ARGUMENTS WITH out AND ref If you want to specify the name of an argument for a ref or out parameter, you put the ref or out modifier after the name, and before the argument. So using int.TryParse as an example, you might have code like this:

int number;

bool success = int.TryParse("10", result: out number);

To explore some other aspects of the syntax, the following listing shows a method with three integer parameters, just like the one we used to start looking at optional parameters.

Listing 3 Simple examples of using named arguments

static void Dump(int x, int y, int z) #1

{

Console.WriteLine("x={0} y={1} z={2}", x, y, z);

}

...

Dump(1, 2, 3); #2

Dump(x: 1, y: 2, z: 3); #3

Dump(z: 3, y: 2, x: 1);

Dump(1, y: 2, z: 3); #4

Dump(1, z: 3, y: 2);

#1 Declares method as normal

#2 Calls method as normal

#3 Specifies names for all arguments

#4 Specifies names for some arguments

The output is the same for each call in listing 3: x=1, y=2, z=3. We’ve effectively made the same method call in five different ways. It’s worth noting that there are no tricks in the method declaration (#1): you can use named arguments with any method that has parameters. First we call the method in the normal way, without using any new features (#2). This is a sort of control point to make sure that the other calls really are equivalent. We then make two calls to the method using just named arguments (#3). The second of these calls reverses the order of the arguments, but the result is still the same, because the arguments are matched up with the parameters by name, not position. Finally there are two calls using a mixture of named arguments and positional arguments (#4). A positional argument is one that isn’t named—so every argument in valid C# 3 code is a positional argument from the point of view of C# 4. Figure 2 shows how the final line of code works.

Figure 2 Positional and named arguments in the same call

All named arguments have to come after positional arguments—you can’t switch between the styles. Positional arguments always refer to the corresponding parameter in the method declaration—you can’t make positional arguments skip a parameter by specifying it later with a named argument. This means that these method calls would both be invalid:

§ Dump(z: 3, 1, y: 2)—Positional arguments must come before named ones.

§ Dump(2, x: 1, z: 3)x has already been specified by the first positional argument, so we can’t specify it again with a named argument.

Now, although in this particular case the method calls have been equivalent, that’s not always the case. Let’s look at why reordering arguments might change behavior.

Argument evaluation order

We’re used to C# evaluating its arguments in the order they’re specified—which, until C# 4, has always been the order in which the parameters have been declared too. In C# 4, only the first part is still true: the arguments are still evaluated in the order they’re written, even if that’s not the same as the order in which they’re declared as parameters. This matters if evaluating the arguments has side effects. It’s usually worth trying to avoid having side effects in arguments, but there are cases where it can make the code clearer. A more realistic rule is to try to avoid side effects that might interfere with each other. For the sake of demonstrating execution order, we’ll break both of these rules. Please don’t treat this as a recommendation that you do the same thing.

First we’ll create a relatively harmless example, introducing a method that logs its input and returns it—a sort of logging echo. We’ll use the return values of three calls to this to call the Dump method (which isn’t shown, as it hasn’t changed). Listing 4 shows two calls to Dump that result in slightly different output.

Listing 4 Logging argument evaluation

static int Log(int value)

{

Console.WriteLine("Log: {0}", value);

return value;

}

...

Dump(x: Log(1), y: Log(2), z: Log(3));

Dump(z: Log(3), x: Log(1), y: Log(2));

The results of running listing 4 show what happens:

Log: 1

Log: 2

Log: 3

x=1 y=2 z=3

Log: 3

Log: 1

Log: 2

x=1 y=2 z=3

In both cases, the parameters in the Dump method are still 1, 2, and 3, in that order. But we can see that although they were evaluated in that order in the first call (which was equivalent to just using positional arguments), the second call evaluated the value used for the z parameter first. We can make the effect even more significant by using side effects that change the results of the argument evaluation, as shown in the following listing, again using the same Dump method.

Listing 5 Abusing argument evaluation order

int i = 0;

Dump(x: ++i, y: ++i, z: ++i);

i = 0;

Dump(z: ++i, x: ++i, y: ++i);

The results of listing 5 may be best expressed in terms of the blood spatter pattern at a murder scene, after someone maintaining code like this has gone after the original author with an axe. Yes, technically speaking the last line prints out x=2 y=3 z=1 but I’m sure you see what I’m getting at. Just say “no” to code like this. By all means, reorder your arguments for the sake of readability: you may think that laying out a call to MessageBox.Show with the title coming above the text in the code itself reflects the onscreen layout more closely, for example. If you want to rely on a particular evaluation order for the arguments, though, introduce some local variables to execute the relevant code in separate statements. The compiler won’t care either way—it’ll follow the rules of the spec—but this reduces the risk of a “harmless refactoring” that inadvertently introduces a subtle bug.

To return to cheerier matters, let’s combine the two features (optional parameters and named arguments) and see how much tidier the code can be.

Putting the two together

The two features work in tandem with no extra effort required on your part. It’s not uncommon to have a bunch of parameters where there are obvious defaults, but where it’s hard to predict which ones a caller will want to specify explicitly. Figure 3 shows just about every combination: a required parameter, two optional parameters, a positional argument, a named argument, and a missing argument for an optional parameter.

Figure 3 Mixing named arguments and optional parameters

Going back to an earlier example, in listing 2 we wanted to append a timestamp to a file using the default encoding of UTF-8, but with a particular timestamp. Back then we just used null for the encoding argument, but now we can write the same code more simply, as shown in the following listing.

Listing 6 Combining named and optional arguments

static void AppendTimestamp(string filename,

string message,

Encoding encoding = null,

DateTime? timestamp = null)

{

#A

}

...

AppendTimestamp("utf8.txt", "Message in the future", #B

timestamp: new DateTime(2030, 1, 1)); #C

#A Same implementation as before

#B Encoding is omitted

#C Named timestamp argument

In this fairly simple situation, the benefit isn’t particularly huge, but in cases where you want to omit three or four arguments but specify the final one, it’s a real blessing.

We’ve seen how optional parameters reduce the need for huge long lists of overloads, but one specific pattern where this is worth mentioning is with respect to immutability.

Immutability and object initialization

One aspect of C# 4 that disappoints me somewhat is that it hasn’t done much explicitly to make immutability easier. Immutable types are a core part of functional programming, and C# has been gradually supporting the functional style more and more... except for immutability. Object and collection initializers make it easy to work with mutable types, but immutable types have been left out in the cold. (Automatically implemented properties fall into this category too.) Fortunately, though they’re not particularly designed to aid immutability, named arguments and optional parameters allow you to write object initializer–like code that just calls a constructor or other factory method. For instance, suppose we were creating a Message class, which required a from address, a to address, and a body, with the subject and attachment being optional. (We’ll stick with single recipients in order to keep the example as simple as possible.) We could create a mutable type with appropriate writable properties, and construct instances like this:

Message message = new Message {

From = "skeet@pobox.com",

To = "csharp-in-depth-readers@everywhere.com",

Body = "Hope you like the second edition",

Subject = "A quick message"

};

That has two problems: first, it doesn’t enforce the required data to be provided. We could force those to be supplied to the constructor, but then (before C# 4) it wouldn’t be obvious which argument meant what:

Message message = new Message(

"skeet@pobox.com",

"csharp-in-depth-readers@everywhere.com",

"Hope you like the second edition")

{

Subject = "A quick message"

};

The second problem is that this initialization pattern simply doesn’t work for immutable types. The compiler has to call a property setter after it has initialized the object. But we can use optional parameters and named arguments to come up with something that has the nice features of the first form (only specifying what you’re interested in and supplying names) without losing the validation of which aspects of the message are required or the benefits of immutability. The following listing shows a possible constructor signature and the construction step for the same message as before.

Listing 7 Constructing an immutable message using C# 4

public Message(string from, string to,

string body, string subject = null,

byte[] attachment = null)

{

#A

}

...

Message message = new Message(

from: "skeet@pobox.com",

to: "csharp-in-depth-readers@everywhere.com",

body: "Hope you like the second edition",

subject: "A quick message"

);

#A Normal initialization code goes here

I really like this in terms of readability and general cleanliness. You don’t need hundreds of constructor overloads to choose from, just one with some of the parameters being optional. The same syntax will also work with static creation methods, unlike object initializers. The only downside is that it really relies on your code being consumed by a language that supports optional parameters and named arguments; otherwise callers will be forced to write ugly code to specify values for all the optional parameters. Obviously there’s more to immutability than getting values to the initialization code, but this is a welcome step in the right direction nonetheless.

There are a couple of final points to make around these features before we move on to COM, both around the details of how the compiler handles our code and the difficulty of good API design.

Overload resolution

Clearly both named arguments and optional parameters affect how the compiler resolves overloads—if there are multiple method signatures available with the same name, which should it pick? Optional parameters can increase the number of applicable methods (if some methods have more parameters than the number of specified arguments) and named arguments can decrease the number of applicable methods (by ruling out methods that don’t have the appropriate parameter names).

For the most part, the changes are intuitive: to check whether any particular method is applicable, the compiler tries to build a list of the arguments it would pass in, using the positional arguments in order, then matching the named arguments up with the remaining parameters. If a required parameter hasn’t been specified or if a named argument doesn’t match any remaining parameters, the method isn’t applicable.

There are two situations I’d like to draw particular attention to. First, if two methods are both applicable and one of them has been given all of its arguments explicitly whereas the other uses an optional parameter filled in with a default value, the method that doesn’t use any default values will win. But this doesn’t extend to just comparing the number of default values used—it’s a strict “does it use default values or not” divide. For example, consider the following:

static void Foo(int x = 10) {}

static void Foo(int x = 10, int y = 20) {}

...

Foo(); #1

Foo(1); #2

Foo(y: 2); #3

Foo(1, 2); #4

#1 Error: ambiguous

#2 Calls first overload

#3 Calls second overload

#4 Calls second overload

In the first call (#1), both methods are applicable because of their optional parameters. But the compiler can’t work out which one you meant to call: it’ll raise an error. In the second call (#2), both methods are still applicable, but the first overload is used because it can be applied without using any default values, whereas the second overload uses the default value for y. For both the third and fourth calls, only the second overload is applicable. The third call (#3) names the y argument, and the fourth call (#4) has two arguments; both of these mean the first overload isn’t applicable.

OVERLOADS AND INHERITANCE DON’T ALWAYS MIX NICELY All of this is assuming that the compiler has gone as far as finding multiple overloads to choose between. If some methods are declared in a base type, but there are applicable methods in a more derived type, the latter will win. This has always been the case, and it can cause some surprising results (see http://mng.bz/aEmE)... but now optional parameters mean there may be more applicable methods than you’d expect. I advise you to avoid overloading a base class method within a derived class unless you get a huge benefit.

The second point is that sometimes named arguments can be an alternative to casting in order to help the compiler resolve overloads. Sometimes a call can be ambiguous because the arguments can be converted to the parameter types in two different methods, but neither method is better than the other in all respects. For instance, consider the following method signatures and a call:

void Method(int x, object y) { ... }

void Method(object a, int b) { ... }

...

Method(10, 10); #A

#A Ambiguous call

Both methods are applicable, and neither is better than the other. There are two ways to resolve this, assuming you can’t change the method names to make them unambiguous that way. (That’s my preferred approach. Make each method name more informative and specific, and the general readability of the code can go up.) You can either cast one of the arguments explicitly, or use named arguments to resolve the ambiguity:

void Method(int x, object y) { ... }

void Method(object a, int b) { ... }

...

Method(10, (object) 10); #A

Method(x: 10, y: 10); #B

#A Casting to resolve ambiguity

#B Naming to resolve ambiguity

Of course this only works if the parameters have different names in the different methods—but it’s a handy trick to know. Sometimes the cast will give more readable code; sometimes the name will. It’s just an extra weapon in the fight for clear code. It does have a downside, along with named arguments in general: it’s another thing to be careful about when you change a method.

The silent horror of changing names

In the past, parameter names haven’t mattered much if you’ve only been using C#. Other languages may have cared, but in C# the only times that parameter names were important were when you were looking at IntelliSense and when you were looking at the method code itself. Now, the parameter names of a method are effectively part of the API even if you’re only using C#. If you change them at a later date, code can break—anything that was using a named argument to refer to one of your parameters will fail to compile if you decide to change it. This may not be much of an issue if your code is only consumed by itself anyway, but if you’re writing a public API, be aware that changing a parameter name is a big deal. It always has been really, but if everything calling the code was written in C#, we’ve been able to ignore that until now.

Renaming parameters is bad; switching the names around is worse. That way the calling code may still compile, but with a different meaning. A particularly evil form of this is to override a method and switch the parameter names in the overridden version. The compiler will always look at the deepest override it knows about, based on the static type of the expression used as the target of the method call. You don’t want to get into a situation where calling the same method implementation with the same argument list results in different behavior based on the static type of a variable.

Summary

Named arguments and optional parameters are possibly two of the simplest-sounding features of C# 4—and yet they still have a fair amount of complexity, as we’ve seen. The basic ideas are easily expressed and understood—and the good news is that most of the time that’s all you need to care about. You can take advantage of optional parameters to reduce the number of overloads you write, and named arguments can make code much more readable when several easily confusable arguments are used. The trickiest bit is probably deciding which default values to use, bearing in mind potential versioning issues. Likewise it’s now more obvious than before that parameter names matter, and you need to be careful when overriding existing methods, to avoid being evil to your callers.


[1] Or Cavalleria Rusticana and Pagliacci if you’re feeling more highly cultured.

[2] Unless you’re using named arguments, of course—we’ll learn about those soon.

[3] Or you could just accept that you’ll need to recompile everything if you change the value. In many contexts that’s a reasonable tradeoff.

[4] We almost need a second null-like special value, meaning “please use the default value for this parameter”—and allow that special value to be supplied either automatically for missing arguments or explicitly in the argument list. I’m sure this would cause dozens of problems, but it’s an interesting thought experiment.

By Peter Bromberg   Popularity  (3459 Views)