[CLR via C#]9. 参数

原文:[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才有意义。
 
四、向方法传递可变数量的参数
  有的时候,开发人员想定义一个方法来获取可变数量的参数。为了声明方法接受可变数量的参数,如下:
   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());
   }

 

五、参数和返回类型的指导原则

  1)声明方法的参数类型时,应尽量指定最弱的类型,最好是接口而不是基类。    
  例如,如果要写一个方法处理一组数据项,最好是用接口(比如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没有提供对常量参数/对象的支持。
 
上一篇:[ARM-assembly]-全局变量/静态全局变量/初始化/未初始化变量的存放位置分析


下一篇:DIRECTSHOW在VS2005中PVOID64问题和配置问题