[CLR via C#]9. 参数
一、可选参数和命名参数
在设计一个方法的参数时,可为部分或全部参数分配默认值。然后,调用这些方法的代码时可以选择不指定部分实参,接受默认值。此外,调用方法时,还可以通过指定参数名称的方式为其传递实参。比如:
internal static class Program { private static Int32 s_n = 0; private static void M(Int32 x=9, String s = "A", DateTime dt = default(DateTime), Guid guid = new Guid()) { Console.WriteLine("x={0}, s={1}, dt={2}, guid={3}, x, s, dt, guid"); } public static void Go() { // 1.等同于: M(9, "A", default(DateTime), new Guid()); M(); // 2. 等同于: M(8, "X", default(DateTime), new Guid()); M(8, "X"); // 3. 等同于: M(5, "A", DateTime.Now, Guid.NewGuid()); M(5, guid: Guid.NewGuid(), dt: DateTime.Now); // 4. 等同于: M(0, "1", default(DateTime), new Guid()); M(s_n++, s_n++.ToString()); // 5. 等同于s: String t1 = "2"; Int32 t2 = 3; // M(t2, t1, default(DateTime), new Guid()); M(s: (s_n++).ToString(), x: s_n++); }
}
在定义的方法中,如果为部分参数指定了默认值,需注意下述原则:
1)可以为方法、构造器方法和有参属性(C#索引器)的参数指定默认值。还可为属于委托定义一部分的参数指定默认值。然后,在调用该委托类型的一个变量时,可以省略实参,以接受默认值。
2)有默认值的参数必须放在没有默认值的所有参数之后。换言之,一旦定义了一个有默认值的参数,它右边的所有参数也必须有默认值。但有个例外:"参数数组"这种参数必须放在所有参数(包括有默认值的这些)之后,而且数组本身不能有一个默认值。
3)默认值必须是编译时能确定的常量值。这些参数的类型可以是C#认定的基元类型,还包括枚举类型,以及设为null的任何引用类型。对于任何值类型的一个参数,可将默认值设为值类型的一个实例,并让它的所有字段都包含零值。可以用default关键字或者new关键字来表达这个意思。如在M方法中设置dt参数和guid参数的默认值,就是用的这两种语法。
4)注意不要重新命名(即修改)参数变量名称。否则,任何调用者如果以传参数名的方式传递实参,都必须修改它们的代码。
5)如果方法是从模块的外部调用的,更改参数的默认值具有潜在的危险性。调用方会在它的调用中嵌入默认值。如果以后更改参数的默认值,但没有重新编译调用方所在的代码,它在调用你的方法时就会传递就得默认值。可考虑将默认值设为0/null作为哨兵值(起到占位子作用)使用。
6)如果参数使用ref或out关键字进行了标识,就不能设置默认值。因为没有办法为这些参数传递一个有意义的默认值。
使用可选或命名参数调用一个方法时,还要注意下述原则:
1)实参可按任何顺序传递;但是,命名实参只能出现在实参列表的尾部。
2)可按名称将实参传给没有默认值的参数。
3)C#不允许省略都好之间的实参,比如M(1, ,DateTime.Now)。
4)如果参数需要ref/out,为了以传参数名的方式传递实参,请使用下面语法:
// 方法声明 private static void M(ref Int32 x) { ... } // 方法调用 Int32 a = 5; M(x: ref a); .....
在C#中,一旦为某个参数分配了一个默认值,编译器就会在内部像该参数应用一个定制attibute,即System.Runtime.InteropServices.OptionalAttribute。这个attribute会在最终生成的文件的元数据中持久性地存储下来。此外,编译器还会向参数引用一个名为System.Runtime.InteropServices.DefaultParameterValueAttribute的attribute,并将这个attribute持久性存储在最终文件的元数据中,然后,会向DefaultParameterValueAttribute的构造器中传递你在源代码中指定的常量值。之后,一旦编译器发现一个方法调用缺失了部分实参,就可以确定省略的是可选的实参,并从元数据中提取它们的默认值,将这些值自动嵌入调用中。
之后,一旦编译器发现一个方法调用缺失了部分实参,就可以确定省略的是可选的实参,并从元数据中提取它们的默认值,并将这些值自动嵌入调用中。
二、隐式类型的局部变量
针对一个方法中的隐式类型的局部变量,C#允许根据初始化表达式的类型来判断它的类型。
private static void ImplicitlyTypedLocalVariables() { var name = "Jeff"; ShowVariableType(name); // 类型是: System.String // var n = null; // 错误 var x = (Exception)null; // 可以这样写,但没意义 ShowVariableType(x); // 类型是: System.Exception var numbers = new Int32[] { 1, 2, 3, 4 }; ShowVariableType(numbers); // 类型是: System.Int32[] // 针对复杂类型,可减少打字量 var collection = new Dictionary<String, Single>() { { ".NET", 4.0f } }; // 类型是: System.Collections.Generic.Dictionary`2[System.String,System.Single] ShowVariableType(collection); foreach (var item in collection) { // 类型是: System.Collections.Generic.KeyValuePair`2[System.String,System.Single] ShowVariableType(item); } }
隐式类型的局部变量是局部变量,不能用它声明方法的参数。也不能声明一个类型的字段。
用var声明的局部变量只是一种简化语法,它要求编译器根据一个表达式推断具体的数据类型。var关键字只能用于声明方法内部的局部变量,而dynamic关键字可用于局部变量,字段和参数。表达式不能转型为var,但可以转型为dynamic。必须实现初始化化var声明的变量,但无需初始化用dynamic声明的变量。
三、以传递引用的方式向方法传递参数
默认情况下,CLR假定所有的方法参数都是传值的。
传递引用类型的对象时,对一个对象的引用(或者说指向对象的指针)会传给方法。但这个引用(或指针)本身是以传值方式传给方法的。这意味着方法能修改对象,而调用者能看到这些修改。对于值类型的实例,传给方法的是实例的一个副本,这意味着方法将获取它专用的一个值类型实例副本,调用中的实例不受影响。
CLR中允许以传引用而非传值的方式传递参数。在C#中,这是用关键字out和ref。这两个关键字都告诉C#编译器生成的元数据来指明该参数时传引用的。编译器将生成代码来传递参数的地址,而不是传递参数本身。
从CLR角度看,关键字out和ref完全一致。这就是说,无论用哪个关键字,都会生成相同的IL代码。另外,元数据也几乎一致。只有一个bit除外,它用于记录声明方法时指定的是out还是ref。
C#编译器是将者两个关键字区别对待的,而且这个区别决定了有哪个方法负责初始化所引用的对象。
如果方法的参数用out来标记,表明不指望调用者在调用方法之前初始化好了对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。相反,如果方法的参数用ref来标记,调用者就必须在调用方法前初始化参数的值,被调用的方法可以读取值或者写入值。
为值类型使用out和ref,效果等同于以传值的方式传递引用类型。对于值类型,out和ref允许方法操纵单一的值类型实例。调用者必须为实例分配内存,被调用者则操纵该内存中的内容。
对于引用类型,调用代码为一个指针分配内存(该指针指向一个引用类型的对象),被调用者则操纵这个指针。正因为如此,仅当方法"返回"对"方法知道的一个对象"的引用时,为引用类型提供out和ref才有意义。
四、向方法传递可变数量的参数
有的时候,开发人员想定义一个方法来获取可变数量的参数。为了声明方法接受可变数量的参数,如下:
1)声明方法的参数类型时,应尽量指定最弱的类型,最好是接口而不是基类。
private static Int32 Add(params Int32[] values) { Int32 sum = 0; for (Int32 x = 0; x < values.Length; x++) sum += values[x]; return sum; }
params关键字只能应用于方法参数列表的最后一个参数。
我们调用时可以这样:
//显示 "15" Console.WriteLine(Add(new Int32[] { 1, 2, 3, 4, 5 }));
也可以这样:
// 显示 "15" Console.WriteLine(Add(1, 2, 3, 4, 5));
由于params关键字的存在,所以可以这么做。params关键字告诉编译器向参数引用System.ParamArrayAttribute的一个实例。
只有方法的最后一个参数才能用params关键字(ParamArrayAttribute)来标记。另外,这个参数只能标识任意类型的一个一位数组。可为这个参数传递null值,或传递对包含另个元素的一个数组的引用。
// 显示"0" Console.WriteLine(Add()); Console.WriteLine(Add(null));
那么如果写一个方法来获取任意数量、任意类型的参数呢?只需要修改方法原型,让它获取一个Object[]而不是Int32[]。比如
private static void DisplayTypes(params Object[] objects) { foreach (Object o in objects) Console.WriteLine(o.GetType()); }
五、参数和返回类型的指导原则
例如,如果要写一个方法处理一组数据项,最好是用接口(比如IEnumerable<T>)来声明方法的参数,而不要使用强数据类型(比如List<T>)或者更强的接口类型(比如ICollection<T>或IList<T>):
//好 public void MainpulateItems<T>(IEnumerable<T> collection) { ... } //不好 public void MainpulateItems<T>(List<T> collection) { ... } //好:该方法使用弱参数类型 public void ProcessBytes(Stream someStream) { ... } //不好:该方法使用强参数类型 public void ProcessBytes(FileStream someStream) { ... }
2)一般最好将方法的返回类型声明为最强的类型,以免受限于特定类型。例如:
//好:该方法使用强返回值类型 public FileStream ProcessBytes() { ... } //不好:该方法使用弱返回值类型 public Stream ProcessBytes() { ... }
第一个方法是首选的,它允许方法的调用者选择将返回对象视为一个FileStream对象或者一个Stream对象。但是,第二个方法要求调用者将返回对象视为一个Stream对象。总之,确保调用者在调用方法时有尽量大的灵活性,使方法的应用范围更大。
六、常量性
CLR没有提供对常量参数/对象的支持。