Introduction
My purpose in writing this article is two-fold: firstly, I want to learn about the new language features up and coming in C# 4.0 now that Visual Studio 2010 is here; secondly, I'd like to share what I learned.
This article is a bite-sized introduction to named and optional arguments in the forthcoming version of the framework. I'm not going to cover all the new features in C# 4 here, or make an in-depth analysis of the features that I am covering. I have done this in the hope that the article will be clearer for new starters to .NET. If this article is well received (and time permitting!), I will write similar articles giving an introduction to other features in a similar fashion.
The big addition to .NET 4.0 is dynamic programming support through the DLR (Dynamic Language Runtime). .NET has traditionally been statically typed, where objects have class types that are known at run-time, as is the structure of the types themselves. There is, increasingly, interaction with objects that have typing which is not fully known until run-time: Iron Ruby is available for .NET 4 and we interact more often with dynamically typed languages; COM objects are interacted with; .NET types can be accessed reflectively; one extremely common task is to deserialize XML into “object-world”, where we might not want to create or need a statically typed class. Code dealing with any of these normally gives off a “bad smell”, being long winded and often difficult to read.
The second article, The Dynamic Keyword in C# 4.0[^], deals with one of the features to support dynamic typing in C#.
What are Named and Optional Arguments?
Named and Optional Arguments are primarily intended to harmonise some of C#'s language features with those of Visual Basic, and are actually a useful addition to C#. Let’s take the common example of an overloaded method, where the overload specifies a default value where it is not supplied:
public class Foo
{
public Bar(string id)
{
Bar(itemCode, 0);
}
public Bar(string id, int amount)
{
}
}
Overloading a method like this is a common task, but it is uglier than it needs to be. With the Named and Optional Arguments added to C# 4.0, the same code can be written much more expressively:
public class Foo
{
public void Bar(string id, int amount= 0)
{
}
}
With this code, it is still possible to call both Foo(string)
which defaults the amount to 0 and Foo(string, int)
. This is a boon if you have multiple overloads, potentially removing a lot of boilerplate code.
Invalid Syntaxes
Naturally, some invalid syntaxes are defined. The compiler will report an error for the following:
public void foo(int amount = 0, string id) { }
The error reported is as required parameters are not allowed after optional ones. This too causes a syntax error:
public void Foo(string id, int count = 0) { }
public void foo(string id = “Foo”, int count = 0) { }
These methods, if written together will fail as the method signatures are the same, effectively defining the method twice. The two invalid syntaxes are defined so that consistent overload resolution can be achieved.
Named Arguments
This method is intended to set the initial position on three axes, defaulting to [0, 0, 0].
public void SetInitialPosition(int x = 0, int y = 0, int z = 0)
{
}
Now suppose we want to set the x and z axes only, one option Microsoft could have chosen is:
SetInitialPosition(0, , 0);
Mercifully, Microsoft did not choose this for their syntax. My example here is not too bad, but if there are many optional arguments it would be hard to keep track of which are being set (e.g.
DoSomething(0, , 0, 0, , 0, , , , 0)
requires the developer to count the position of each parameter). A named syntax was chosen:
SetInitialPosition(0, z: 0);
Two things to be aware of are:
-
SetInitialPosition(1, z: 2, y: 3);
will set “z
” to 2
and “y
” to 3
as expected, even though the order the method declares in is x
, y
, z
. This is potentially confusing!
- If we just wish to set the first two parameters, we can do this by name or position:
SetInitialPosition(6, 9);
SetInitialPosition(6, y:9);
SetInitialPosition(x:6, y:9);
All specify the same thing.
Overload Resolution
Those of you who are still awake at this point will realise there is a potential minefield if the methods themselves are overloaded:
public void OverloadedMethod(string x)
{
}
public void OverloadedMethod(object x)
{
}
public void OverloadedMethod(int x)
{
}
public void OverloadedMethod(int x, string y = "foo")
{
}
public void OverloadedMethod(int x = 0, int y = 0)
{
}
public void OverloadedMethod(int x = 0, int y = 0, int z = 0)
{
}
public void OverloadedMethod(string s = "foo", int x = 0 )
{
}
What happens if I call OverloadedMethod(0);
? The framework firstly applies an applicability test that determines which methods can be called. Then a betterness test determines which of the applicable methods is actually called.
To be applicable, the parameters passed by the call must match (or be directly convertible to) the required arguments of the overload, as per earlier versions of the framework. Only void OverloadedMethod(string x)
fails this test as the value passed (0) is not directly convertible to a string
. Overloads 1-3 pass this criterion as the required argument needs are met. Overloads 4 - 6 also meet this criterion almost “by stealth”, they have no required arguments, so these are met and the argument that is supplied can be matched (by position in this example) to an optional argument. If we had called...
OverloadedMethod(0, a: "bar");
...none of the methods would be applicable, overload 3 would have if we hadn't specified a string
as being destined for argument “a
”. Similarly Overload 6 would become acceptable if we called...
OverloadedMethod(x : 0);
...as the parameter would refer to a named optional argument.
Now that Overloads 0 and 6 have been deemed un-applicable, the framework applies its betterness test. Overload 1 is rejected as the conversion to an object is required, but this is not the case for overloads 2-4 where the int
can be used without conversion. Overloads 3, 4, and 5 are all rejected as the framework favours the call with the fewest number of default values used. This leaves Overload 2 as the one that is called (0 defaults).
As an interesting side-note, if Overload 2 did not exist, what would happen? Overloads 0 & 6 are in-applicable, Overload 1 still requires conversion and overload 5 has more unspecified optional arguments. This leaves Overloads 3 & 4 neck and neck, even in terms of the betterness test. In this case, an ambiguous call error is reported at build time.
Interfaces and Inheritance
Inheritance and Interfaces add a little complexity to the problem, for example, look at this code:
public interface IFoo
{
void Bar(int i);
}
public class Foo : Foo
{
public virtual void Bar(int i=0) { }
}
This code compiles and works as expected, the class Foo
implements the method Bar(int i)
defined in the interface. As it is valid to call Bar();
you might expect to be able to re-define the interface to the following:
public interface IFoo
{
void Bar();
void Bar(int i);
}
This interface will not work with the concrete implementation, but why? As the code stands, by calling Bar()
without the integer, the code that ultimately gets called has an integer argument, albeit set to a default value. As such, the compiler reports that the interface member IFoo.Bar()
is not defined in our concrete class. Things get stickier when inheriting from concrete classes, the simple case works as you would expect, following the normal inheritance hierarchy and overload resolution:
public class Foo
{
public virtual void Baz(int i=1) { Console.Write(i); }
}
public class Bar : Foo
{
public override void Baz(int i=2) { Console.Write(i); }
}
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
Bar bar = new Bar();
foo.Baz(); foo.Baz(3); bar.Baz(); bar.Baz(4); }
}
As we discovered earlier, the parameterless method Baz()
isn't implemented as far as the compiler is concerned, so the following change is valid, as we don't need a Baz()
in the base class to override:
public class Foo
{
public virtual void Baz(int i) { Console.Write(i); }
}
Similarly, the following won't compile, as the base class does not have a Baz(int i)
method to override:
public class Foo
{
public virtual void Baz() { }
}
public class Bar : Foo
{
public override void Baz(int i=2) { }
}
Conversely, the following will run happily:
public class Foo
{
public virtual void Baz() { Console.Write("Base"); }
public virtual void Baz(int i) { Console.Write(i); }
}
public class Bar : Foo
{
public override void Baz(int i=2) { Console.Write(i); }
}
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
Bar bar = new Bar();
foo.Baz(); foo.Baz(3); bar.Baz(); bar.Baz(4); }
}
This can catch out the unwary, why does bar.Baz();
output base? The answer is the base method Baz()
in Foo
passes the betterness test over than the named method Baz(int i=2)
in Bar
Conclusion
Named and optional arguments are a very useful, expressive addition to the C# language. Optional augments allow us to write overloaded code in a far terser way than has been the case in previous versions, removing a lot of boilerplate code. Named arguments allow us to specify optional arguments “out of order”, preventing the need to supply placeholders for all arguments (and the attendant comma – counting to keep track), but does mean we need to be aware that a named argument called is not necessarily supplied in the same order as the method declaration, though in well written code this should not occur.
History
- 21st February, 2010
- 13th April, 2010
- Corrected slight factual error
- Added Inheritance and Interfaces section
- Added link to second article