为什么需要开放命名参数和可选参数呢?
•这是出于动态语言运行时兼容性的要求。动态语言中存在动态绑定的参数列表,有时候并不是所有的参数值都需要指定(有些语言可能没有重载决策);
•另外,在一些 COM 互操作时,往往 COM Invoke 的方法参数列表非常的长(例如 ExcelApplication.Save,可能需要 12 个参数),但 COM 暴露的参数的实际值往往为 null,只有很少一部分参数需要指定植,如 ExcelApplication.Save(),可能不需要指定任何参数值,或者仅仅一个值,例如 fileType。为了精简书写的代码,就有着这个特性。
命名参数和可选参数是两个截然不同的功能,但通常一起使用。在进行成员调用时,可以忽略可选参数;而命名参数的方式可以通过名称来提供一个参数,而无需依赖它在参数列表中出现的位置。
有些API——尤其是COM接口——如Office自动化API——确实本身就是通过命名参数和可选参数编写的。之前在C#中调用这些API非常痛苦,尤其有的时候需要多达30几个参数都必须显式传递,而其中大多数都具有合理的默认值,是可以忽略的。
即便是编写.NET中的API,你也会发现很多时候你在被迫为不同的参数组合方式编写一个方法的大量重载形式,以便给调用者提供最高的可用性。在这种情况下,可选参数就会成为一种非常有用的替代方式。
Named parameters 命名参数
有了命名实参,您将不再需要记住或查找形参在所调用方法的形参列表中的顺序。 可以按形参名称指定每个实参的形参。 例如,可以采用标准方式调用计算身体质量指数 (BMI) 的函数,方法是依照该函数定义的顺序按位置发送体重和身高的实参。
CalculateBMI(123, 64);
如果不记得形参的顺序,但却知道其名称,您可以按任意顺序(先发送体重或先发送身高)发送实参。
CalculateBMI(weight: 123, height: 64);
CalculateBMI(height: 64, weight: 123);
命名实参还可以标识每个实参所表示的含义,从而改进代码的可读性。
命名实参可以放在位置实参后面,如此处所示。
CalculateBMI(123, height: 64);
但是,位置实参不能放在命名实参后面。 下面的语句会导致编译器错误。
//CalculateBMI(weight: 123, 64);
01 class NamedExample
02 {
03 static void Main(string[] args)
04 {
05 // The method can be called in the normal way, by using positional arguments.
06 Console.WriteLine(CalculateBMI(123, 64));
07
08 // Named arguments can be supplied for the parameters in either order.
09 Console.WriteLine(CalculateBMI(weight: 123, height: 64));
10 Console.WriteLine(CalculateBMI(height: 64, weight: 123));
11
12 // Positional arguments cannot follow named arguments.
13 // The following statement causes a compiler error.
14 //Console.WriteLine(CalculateBMI(weight: 123, 64));
15
16 // Named arguments can follow positional arguments.
17 Console.WriteLine(CalculateBMI(123, height: 64));
18 }
19
20 static int CalculateBMI(int weight, int height)
21 {
22 return (weight * 703) / (height * height);
23 }
24 }
Named Parameters 的实质就是:
命名参数,无非是变相告知参数键值而已,最终编译结果还是按照原有的规则和顺序生成方法调用。这只不过是中语法糖而已。
Optional parameters 可选参数
为一个参数提供默认值就可以将其声明为可选的——
1 public void M(int x, int y = 5, int z = 7);
这里的y和z就是可选参数,在调用时可以忽略——
1 M(1, 2, 3); // ordinary call of M
2 M(1, 2); // omitting z – equivalent to M(1, 2, 7)
3 M(1); // omitting both y and z – equivalent to M(1, 5, 7)
方法、构造函数、索引器或委托的定义可以指定其形参为必需还是可选。 任何调用都必须为所有必需的形参提供实参,但可以为可选的形参省略实参。
每个可选形参都具有默认值作为其定义的一部分。 如果没有为该形参发送实参,则使用默认值。 默认值必须是一个表达式的以下类型之一:
•常数表达式;
•窗体 new ValType()的表达式, ValType 是值类型,例如 枚举 或 结构;
•窗体 默认 (ValType)的表达式, ValType 是值类型。
可选形参在形参列表的末尾定义,位于任何必需的形参之后。 如果调用方为一系列可选形参中的任意一个形参提供了实参,则它必须为前面的所有可选形参提供实参。 实参列表中不支持使用逗号分隔的间隔。 例如,在以下代码中,使用一个必选形参和两个可选形参定义实例方法 ExampleMethod。
1 public void ExampleMethod(int required, string optionalstr = "default string", int optionalint = 10)
下面对 ExampleMethod 的调用导致编译器错误,原因是为第三个形参而不是为第二个形参提供了实参。
1 //anExample.ExampleMethod(3, ,4);
但是,如果您知道第三个形参的名称,则可以使用命名实参来完成任务。
1 anExample.ExampleMethod(3, optionalint: 4);
IntelliSense 使用括号指示可选形参,如下图所示。
还可以通过使用 .NET OptionalAttribute 类来声明可选形参。 OptionalAttribute 形参不需要默认值。
在下面的示例中,ExampleClass 的构造函数具有一个形参,该形参是可选的。 实例方法 ExampleMethod具有一个必需的形参:required,以及两个可选形参:optionalstr 和 optionalint。 Main 中的代码演示了可用于调用构造函数和方法的不同方式。
01 namespace OptionalNamespace
02 {
03 class OptionalExample
04 {
05 static void Main(string[] args)
06 {
07 // Instance anExample does not send an argument for the constructor's
08 // optional parameter.
09 ExampleClass anExample = new ExampleClass();
10 anExample.ExampleMethod(1, "One", 1);
11 anExample.ExampleMethod(2, "Two");
12 anExample.ExampleMethod(3);
13
14 // Instance anotherExample sends an argument for the constructor's
15 // optional parameter.
16 ExampleClass anotherExample = new ExampleClass("Provided name");
17 anotherExample.ExampleMethod(1, "One", 1);
18 anotherExample.ExampleMethod(2, "Two");
19 anotherExample.ExampleMethod(3);
20
21 // The following statements produce compiler errors.
22
23 // An argument must be supplied for the first parameter, and it
24 // must be an integer.
25 //anExample.ExampleMethod("One", 1);
26 //anExample.ExampleMethod();
27
28 // You cannot leave a gap in the provided arguments.
29 //anExample.ExampleMethod(3, ,4);
30 //anExample.ExampleMethod(3, 4);
31
32 // You can use a named parameter to make the previous
33 // statement work.
34 anExample.ExampleMethod(3, optionalint: 4);
35 }
36 }
37
38 class ExampleClass
39 {
40 private string _name;
41
42 // Because the parameter for the constructor, name, has a default
43 // value assigned to it, it is optional.
44 public ExampleClass(string name = "Default name")
45 {
46 _name = name;
47 }
48
49 // The first parameter, required, has no default value assigned
50 // to it. Therefore, it is not optional. Both optionalstr and
51 // optionalint have default values assigned to them. They are optional.
52 public void ExampleMethod(int required, string optionalstr = "default string",
53 int optionalint = 10)
54 {
55 Console.WriteLine("{0}: {1}, {2}, and {3}.", _name, required, optionalstr,
56 optionalint);
57 }
58 }
59
60 // The output from this example is the following:
61 // Default name: 1, One, and 1.
62 // Default name: 2, Two, and 10.
63 // Default name: 3, default string, and 10.
64 // Provided name: 1, One, and 1.
65 // Provided name: 2, Two, and 10.
66 // Provided name: 3, default string, and 10.
67 // Default name: 3, default string, and 4.
68
69 }
Optional Parameters 的实质就是:
编译器为可选参数增加了 OptionalAttribute 和 DefaultParameterValueAttribute 特性,以便于引用编译和反射调用时能获取默认值。
至于不用 OptionalAttribute 和 DefaultParameterValueAttribute 特性的写法只不过是种语法糖而已。
Named and optional arguments 命名的和可选的实参
C# 4.0不允许忽略逗号之间的实参,比如M(1,,3)。否则会导致大量不可读的、需要“数逗号”的代码。替代方式是任何参数都可以通过名字传递。因此如果在调用M时只希望忽略y,可以写——
1 M(1, z: 3); // passing z by name
或
1 M(x: 1, z: 3); // passing both x and z by name
或
1 M(z: 3, x: 1); // reversing the order of arguments
这几种形式都是等价的,不过参数总是按照其出现的顺序进行求值,因此对于最后一个示例来说,3会在1之前求值。
可选参数和命名参数不仅可以用在方法调用中,还可以用在索引器和构造器中。
COM接口
命名实参和可选实参,以及对动态对象的支持和其他增强功能大大提高了与 COM API(例如 Office 自动化 API)的互操作性。
例如,Microsoft Office Excel 的 Range 接口中的 AutoFormat 方法具有七个形参,这七个形参都是可选的。这些形参如下图所示。
AutoFormat 形参
在 C# 3.0 和早期版本中,每个形参都需要一个实参,如以下示例所示。
01 // In C# 3.0 and earlier versions, you need to supply an argument for
02 // every parameter. The following call specifies a value for the first
03 // parameter, and sends a placeholder value for the other six. The
04 // default values are used for those parameters.
05 var excelApp = new Microsoft.Office.Interop.Excel.Application();
06 excelApp.Workbooks.Add();
07 excelApp.Visible = true;
08
09 var myFormat =
10 Microsoft.Office.Interop.Excel.XlRangeAutoFormat.xlRangeAutoFormatAccounting1;
11
12 excelApp.get_Range("A1", "B4").AutoFormat(myFormat, Type.Missing,
13 Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);
但是,可以通过使用 C# 4.0 中引入的命名实参和可选实参来大大简化对 AutoFormat 的调用。 如果不希望更改形参的默认值,则可以通过使用命名实参和可选实参来为可选形参省略实参。 在下面的调用中,仅为七个形参中的其中一个指定了值。
1 // The following code shows the same call to AutoFormat in C# 4.0. Only
2 // the argument for which you want to provide a specific value is listed.
3 excelApp.Range["A1", "B4"].AutoFormat( Format: myFormat );
Overload resolution 重载解析
命名参数和可选参数影响了重载解析,但产生的变化相当简单——
如果所有的参数或者是可选的,或者在调用时(通过名字或位置)明确提供了对应的实参,并且实参能够转换为形参类型,则该签名是可适用的(applicable)。
转换的最优原则只用于明确给定的实参——出于最优的目的,忽略掉的可选参数在重载解析时将不做考虑。
如果两个签名一样好,则没有忽略可选参数的那个胜出。
1 M(string s, int i = 1);
2 M(object o);
3 M(int i, string s = “Hello”);
4 M(int i);
5
6 M(5);
对于给定的这些重载,我们可以看看上述规则的工作方式。M(string,int)不是可适用的,因为5不能转换为string。M(int,string)是可适用的,因为它的第二个参数是可选的,然后很明显,M(object)和M(int)也是可适用的。
M(int,string)和M(int)都比M(object)要好,因为将5转换为int优于将5转换为object。
最后,M(int)优于M(int,string),因为它没有被忽略的可选参数。
因此,最终调用的方法是M(int)。
使用命名实参和可选实参将在以下方面对重载决策产生影响:
•如果方法、索引器或构造函数的各个形参均为可选,或者按名称或位置与调用语句中的单个实参对应,并且该实参可转换为形参的类型,则该方法、索引器或构造函数是执行的候选项。
•如果找到多个候选项,则会将首选转换的重载决策规则应用于显式指定的实参。 将忽略可选形参已省略的实参。
•如果两个候选项不相上下,则会将没有可选形参的候选项作为首选项,对于这些可选形参,已在调用中为其省略了实参。 这是具有较少形参的候选项的重载决策中一般首选项的结果。