参数

可选参数和命名参数

  1. 设计方法参数时,可以为部分或全部参数分配默认值.
class CLR可选参数
{
    private static int s_n = 0;

    private static void M(int x = 9, string s = "A", DateTime dt = default(DateTime), Guid guid = new Guid())
    {
        Console.WriteLine($"x={x},s={s},dt={dt},guid={guid}");
    }

    public static void Main()
    {
        //等同于M(9, "A", default(DateTime), new Guid())
        M();
        //等同于M(8, "X", default(DateTime), new Guid())
        M(8, "X");
        //等同于M(5, "A", DateTime.Now, Guid.NewGuid()))
        M(5, guid: Guid.NewGuid(), dt: DateTime.Now);

        M(s_n++,s_n++.ToString());

        //等同于string t1=0,t2=1;
        //M(t2,t1,default(DateTime), new Guid())
        M(s: (s_n++).ToString(), x: s_n++);
    }
}
//输出
//x=9,s=A,dt=0001/1/1 0:00:00,guid=00000000-0000-0000-0000-000000000000
//x=8,s=X,dt=0001/1/1 0:00:00,guid=00000000-0000-0000-0000-000000000000
//x=5,s=A,dt=2019/7/31 11:03:00,guid=afb6f487-dc69-4ae4-ba58-588982d1e68a
//x=0,s=1,dt=0001/1/1 0:00:00,guid=00000000-0000-0000-0000-000000000000
//x=3,s=2,dt=0001/1/1 0:00:00,guid=00000000-0000-0000-0000-000000000000

部分参数指定默认值的规则和原则

  1. 可以为方法,构造器方法,有参属性(C#索引器)的参数指定默认值.
  2. 有默认值的参数必须放在没有默认值的所有参数之后.
  3. “参数数组”必须放到所有参数之后(包括有默认值的这些),而数组本身不能由默认值.
  4. 默认值必须是编译时能确定的常量值.
    1. 值类型参数可将默认值设为值类型的实例. default()关键字和new关键字会生成完全一致的IL代码.DateTime dt = default(DateTime), Guid guid = new Guid()
  5. 不要重命名参数变量,不然之后的指定赋值时变量名就需要修改.s: (s_n++).ToString(), x: s_n++就会报错.
  6. 更改参数的默认值具有潜在危险性. call site在它的调用中嵌入默认值, 建议将默认值0/null作为哨兵值使用.
    1. 如果方法被模块外部调用,外部使用默认值,如果以后修改方法的默认值,没有重新编译call site外部的代码.那它在调用方法时,传递的还是旧的默认值.
// 不要这样做
private static String MakePath(String filename = "Untitled")
// 参数中的默认值会被直接嵌入到方法调用中的IL代码里
// 如果修改默认参数后,call site代码没有重新编译,仍旧会使用旧默认值
{
  return String.Format($"C:\{filename}.txt");
}

// 建议这样做
// 默认值用0/null作为哨兵值使用
private static String MakePath(String filename = null)
{
  // 在语句中就不会出现这样的问题
  return String.Format("C:\{0}.txt", filename ?? "Untitled");
}
  1. 如果参数用ref或out关键字进行标识,就不能设置默认值. 没办法为这些参数传递有意义的默认值.

使用可选或命名参数调用方法时,

  1. 实参可按任意顺序传递,命名实参只能出现在实参列表的尾部.( x: s_n++ 命名实参:值)
  2. C#不允许省略逗号之间的实参.(如果想省略, 就用要以传递参数名:的方式传递实参就可以)
  3. 如果参数要求ref/out, 为了以传参数名的方式传递实参,使用以下语法
// 方法声明
private static void M(ref Int32 x){ ... }

// 方法调用
Int32 a = 5;
M(x: ref a);

隐式类型( var )的局部变量

  1. 不能用var声明参数,因为编译器必须根据在call site传递的实参来推断类型,但是call site可能一处没有,也可能有很多处.
  2. 不能用var声明字段. 应该显式陈述.

不要混淆dynamic和var
用var声明局部变量只是一种简化语法.

以传引用的方式向方法传递参数

CLR默认所有方法参数都传值. 传递引用类型的对象时, 引用(或指针)本身是传值的.

  • 引用类型: 方法能修改对象,而调用者能看到这些修改
  • 值类型:传给方法的是实例的一个副本,调用者的实例不受影响.

ref和out关键字

CLR运行以传引用而非传值的方式传递参数. C#用关键字out/ref来支持这个功能. 两个关键字都告诉C#编译器生成元数据来指明该参数时传引用的. 编译器会生成代码来传递参数的地址,而不是传递参数本身.

CLR不区分关键字out/ref,都会生成相同的IL代码,元数据几乎完全一致,只有一个bit除外,记录指定的是out还是ref.

C#编译器则是区别对待out/ref的.

  • out标记:被传入调用方法的参数 不必要初始化, 被调用的方法 不能读取参数值, 在返回前
    必须向这个值写入.
  • ref标记:调用方法前 必须先初始化被ref标记的参数, 被调用的方法 可以读写 这个参数值.

值类型使用ref和out关键字

public static void Main()
{
    int x;                //x没有初始化
    GetVal(out x);        //x不必初始化
    Console.WriteLine(x); //显示10
}

private static void GetVal(out int v)
{
    // 访问前必须先赋值v
    // Console.WriteLine(v); 这句话编译时会报错
    v = 10; //该方法必须初始化v
}
  1. x在Main的栈帧(当前线程的调用栈中的一个方法调用)
  2. x的地址传递给GetVal, GetVal的参数v是一个指针,指向Main栈帧中的Int32值.
  3. 在GetVal方法内部,v指向的那个Int32值被更改为10.

栈帧: 在执行线程的过程中进行的每个方法调用都会在调用栈中创建并压入一个StackFrame

用处: 为 大的值类型使用out, 可以避免在进行方法调用时复制值类型实例的字段.

public static void Main()
{
    int x = 5;            //已经初始化
    AddVal(ref x);        //x必须初始化
    Console.WriteLine(x); //显示15
}

private static void AddVal(ref int v)
{
    v += 10; //该方法可使用v的已初始化的值
}
  1. 和out不同的是,在AddVal内部,v指向的那个Int32值必须是已经初始化的.
  2. AddVal可以对v进行读写

综上所述:

  1. 从IL和CLR角度看,out和ref是同一回事: 都传递指向实例的一个指针.
  2. 从编译器角度看,两者的区别在于:会按不同的标准验证你写的代码是否正确,是否已赋值.
  3. 为值类型使用out/ref,效果等同于以传值的方式传递引用类型.

为什么C#要求必须在调用方法时指定out或ref

C#语言设计者认为调用者应该显式表明意图,只有这样,在call site(调用位置)那里,才能清楚地知道被调用的方法是否需要对传递的变量值进行更改.

C#允许 使用out/ref参数对方法进行重载.但是两个不能同时出现,因为2个签名的元数据形式完全相同.

public sealed class Point
{
  static void Add(Point p){ ... }
  static void Add(ref Point p){ ... }
  // Add不能定义仅在ref和out上有差别的重载方法
  // 因为这两个方法签名生成的元数据形式完全相同
  // static void Add(out Point p) { ... }
}

引用类型使用ref和out关键字

和值类型使用out/ref参数的区别:

  1. 值类型:调用者必须 为实例分配内存. 被调用者则操作该内存中的内容.
  2. 引用类型:调用代码 为一个指针分配内存,指针指向引用类型的对象. 被调用者操纵这个指针.
public static void Main()
{
    FileStream fs = null; // 初始化为null (必要操作)

    // 打开第一个待处理的文件
    for(; fs != null; ProcessFiles(ref fs))
    {
      //处理文件
      fs.Read(...);
    }
}

private static void ProcessFiles(ref FileStream fs)
{
    // 如果先前的文件是打开的,就将其关闭
    if (fs!=null) fs.Close();

    // 打开下一个文件,如果没有,就返回null
    if (noMoreFilesToProcess) fs = null;
    else
    {
        fs = new FileStream(...);
    }
}

是ref关键字实现一个用于交换两个引用类型

public static void Main()
{
    String s1 = "111";
    String s2 = "222";
    // 无法通过编译,参数类型不匹配
    // 它的类型必须与签名中声明的类型相同
    // Swap需要的是两个Object的引用,而不是String
    // Swap(ref s1,ref s2);


    // 需要进行转型
    Object o1 = s1, o2 = s2;
    Swap(ref o1,ref o2);
    // 完事后,再将Object转型为String
    s1 = (String) o1;
    s2 = (String) o2;

}

public static void Swap(ref Object a,ref Object b)
{
    Object t = b;
    b = a;
    a = t;
}

// 泛型版本
// 这样之前就不需要再转型了,能直接通过编译完美运行
public static void Swap<T>(ref T a,ref T b)
{
    T t = b;
    b = a;
    a = t;
}

传递参数的类型必须与签名中声明的类型相同.

向方法传递可变数量的参数(params)

// 用params关键字
static int Add(params int[] values)
{
    int sum = 0;
    if (values != null)
    {
        for (int x = 0; x < values.Length; x++)
            sum += values[x];
    }

    return sum;
}

//调用方法1
Add(new int[]{1,2,3,4,5,6});
//调用方法2,
Add(1,2,3,4,5,6);

params关键字告诉编译器向参数定制特性System.ParamArrayAttribute的一个实例.

C#编译器检测方法调用时,会先检查所有具有指定名称,同时参数没有应用ParamArray的方法.如果没有找到,就检查应用了ParamArray特性的方法,生成代码来构造数组,填充它的元素,再生成代码来调用所选的方法.

  1. 只有方法的最后一个参数才可以用params关键字(ParamArrayAttribute)标记.
  2. 只能标识一维数组(任意类型).
  3. 可为这个参数传递null值. Console.WriteLine(Add(null));//生成的结果是0,不会分配数组

获取任意数量,任意类型的参数方法

只需要修改方法原型,让它获取Object[]而不是Int32[];

static void Main(string[] args)
{
    DisplayTypes(new object(),new Random(),"Jeff",5);
}

private static void DisplayTypes(params Object[] objects)
{
    if (objects!=null)
    {
        foreach (object o in objects)
        {
            Console.WriteLine(o.GetType());
        }
    }
}
// System.Object
// System.Random
// System.String
// System.Int32

使用params会对性能有所影响

  1. 数组必须在堆上分配
  2. 数组元素必须初始化
  3. 数组内存最终需要垃圾回收

参数和返回类型的设计规范

这里例子讨论的是集合,是用接口体系结构来设计的.

  1. 声明方法的参数类型时, 应尽量指定最弱的类型,宁愿要接口也不要基类. 最好是使用接口声明参数比如IEnumerable<T>
// 好: 方法使用弱参数类型
// 参数的类型只要实现了IEnumerable<T>接口就可以
// 方法更灵活,适合更广泛的情形
public void MainpulateItems<T>(IEnumerable<T> collection) { ... }

// 不好: 方法使用强参数类型
// 参数不接受数组或String对象
public void MainpulateItems<T>(List<T> collection) { ... }
  1. 如果方法需要的是列表,就应该将参数类型声明为IList<T> ,避免声明为List<T>
  2. 强参数类型和弱参数类型,参数类型最好是弱类型
  3. 返回类型最好是声明为最强的类型
// 好: 方法使用弱参数类型
// 能处理任何流包括FileStream,NetworkStream,MemoryStream等
public void ProcessBytes(Stream someStream){ ... }

// 不好: 方法使用强参数类型
// 只能处理FileStream
public void ProcessBytes(FileStream someStream){ ... }

// 好: 方法使用强返回类型
public FileStream OpenFile(){ ... }
// 不好: 方法使用弱返回类型
public Stream OpenFile(){ ... }

常量性

  1. 不能将方法或参数声明为常量.
  2. CLR也没提供对常量对象/实参的支持.

String类就没有提供任何能更改String对象的方法,所以字符串是不可变的(immutable).