[CLR via C#]9. 参数

简介: 原文:[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没有提供对常量参数/对象的支持。
 
目录
相关文章
|
7月前
|
存储 编解码 开发工具
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK使用UserSet功能保存和载入相机的各类参数(C#)
Baumer工业相机堡盟工业相机如何通过NEOAPI SDK使用UserSet功能保存和载入相机的各类参数(C#)
99 0
|
6月前
|
存储 Java 编译器
C# 变量与参数详解
C# 变量与参数详解
|
7月前
|
开发框架 小程序 .NET
C#动态生成带参数的小程序二维码
C#动态生成带参数的小程序二维码
|
7月前
|
存储 C#
C# 方法详解:定义、调用、参数、默认值、返回值、命名参数、方法重载全解析
方法是一段代码,只有在调用时才会运行。 您可以将数据(称为参数)传递给方法。 方法用于执行某些操作,也被称为函数。 为什么使用方法?为了重用代码:定义一次代码,然后多次使用。
153 0
|
C# 数据安全/隐私保护
C# 窗体之间参数互相传递的两种方法与使用
C# 窗体之间参数互相传递的两种方法与使用
|
存储 API C#
Baumer工业相机堡盟工业相机如何通过文件保存和导入的方式保存和载入相机的各类参数(C#)
Baumer工业相机堡盟工业相机如何通过文件保存和导入的方式保存和载入相机的各类参数(C#)
88 0
|
存储 编解码 开发工具
Baumer工业相机堡盟工业相机如何通过BGAPI SDK使用UserSet功能保存和载入相机的各类参数(C#)
Baumer工业相机堡盟工业相机如何通过BGAPI SDK使用UserSet功能保存和载入相机的各类参数(C#)
62 0
|
数据挖掘 编译器 C#
【C#本质论 五】方法和参数
【C#本质论 五】方法和参数
70 0
|
数据处理 C#
基于C#的ArcEngine二次开发39:GP工具的使用--界面、参数及示例代码
基于C#的ArcEngine二次开发39:GP工具的使用--界面、参数及示例代码
基于C#的ArcEngine二次开发39:GP工具的使用--界面、参数及示例代码