Optional Parameters
The C# language designers also added support for optional parameters.7 By allowing the association of a parameter with a constant value as part of the method declaration, it is possible to call a method without passing an argument for every parameter of the method (see Listing 5.24).
Listing 5.24: Methods with Optional Parameters
public static class LineCounter { public static void Main(string[] args) { int totalLineCount; if(args.Length > 1) { totalLineCount = DirectoryCountLines(args[0], args[1]); } else if(args.Length > 0) { totalLineCount = DirectoryCountLines(args[0]); } else { totalLineCount = DirectoryCountLines(); } Console.WriteLine(totalLineCount); } static int DirectoryCountLines() { // ... } /* static int DirectoryCountLines(string directory) { ... } */ static int DirectoryCountLines( string directory, string extension = "*.cs") { int lineCount = 0; foreach(string file in Directory.GetFiles(directory, extension)) { lineCount += CountLines(file); } foreach(string subdirectory in Directory.GetDirectories(directory)) { lineCount += DirectoryCountLines(subdirectory); } return lineCount; } private static int CountLines(string file) { // ... } }
In Listing 5.24, the DirectoryCountLines() method declaration with a single parameter has been removed (commented out), but the call from Main() (specifying one parameter) remains. When no extension parameter is specified in the call, the value assigned to extension within the declaration (*.cs in this case) is used. This allows the calling code to not specify a value if desired, and it eliminates the additional overload that would otherwise be required. Note that optional parameters must appear after all required parameters (those that don’t have default values). Also, the fact that the default value needs to be a constant, compile-time–resolved value is fairly restrictive. You cannot, for example, declare a method like
DirectoryCountLines( string directory = Environment.CurrentDirectory, string extension = "*.cs")
because Environment.CurrentDirectory is not a constant. In contrast, because "*.cs" is a constant, C# does allow it for the default value of an optional parameter.
A second method call feature is the use of named arguments.8 With named arguments, it is possible for the caller to explicitly identify the name of the parameter to be assigned a value, rather than relying solely on parameter and argument order to correlate them (see Listing 5.25).
Listing 5.25: Specifying Parameters by Name
public static void Main() { DisplayGreeting( firstName: "Inigo", lastName: "Montoya"); } public static void DisplayGreeting( string firstName, string? middleName = null, string? lastName = null ) { // ... }
In Listing 5.25, the call to DisplayGreeting() from within Main() assigns a value to a parameter by name. Of the two optional parameters (middleName and lastName), only lastName is given as an argument. For cases where a method has lots of parameters and many of them are optional,9 using the named argument syntax is certainly a convenience. However, along with the convenience comes an impact on the flexibility of the method interface. In the past, parameter names could be changed without causing C# code that invokes the method to no longer compile. With the addition of named parameters, the parameter name becomes part of the interface because changing the name would cause code that uses the named parameter to no longer compile.
For many experienced C# developers, this is a surprising restriction. However, the restriction has been imposed as part of the Common Language Specification ever since .NET 1.0. Moreover, Visual Basic has always supported calling methods with named arguments. Therefore, library developers should already be following the practice of not changing parameter names to successfully interoperate with other .NET languages from version to version. In essence, named arguments now impose the same restriction on changing parameter names that many other .NET languages already require.
Given the combination of method overloading, optional parameters, and named parameters, resolving which method to call becomes less obvious. A call is applicable (compatible) with a method if all parameters have exactly one corresponding argument (either by name or by position) that is type-compatible, unless the parameter is optional (or is a parameter array). Although this restricts the possible number of methods that will be called, it doesn’t identify a unique method. To further distinguish which specific method will be called, the compiler uses only explicitly identified parameters in the caller, ignoring all optional parameters that were not specified at the caller. Therefore, if two methods are applicable because one of them has an optional parameter, the compiler will resolve to the method without the optional parameter.