|
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.