9.5 引用参数
CLR假设所有的方法参数都是按值传递,当参数为引用类型的对象时传递的是引用/指针的值,而值类型对象传递的是对象实例的一个拷贝
C#中我们可以用out和ref关键字指定方法按引用的方式传递参数,从IL或CLR的角度来看out和ref关键字的行为实际上是一样的,二者的不同之处在于编译器会根据它们选择不同的机制来确保代码的正确性。开发时指定out和ref的好处是可以清晰的表示开发者的意图
CLR还允许我们根据有无out或ref参数来重载方法,如:
static void Add(Point p) { ... }
static void Add(ref Point p) { ... }
但因为out和ref经JIT编译后的代码是相同的,所以我们不能只通过区分out和ref来重载方法,如不能在定义上面的方法的类型中再定义下面的方法:
static void Add(out Point p) { ... }
在值类型参数上使用out和ref关键字与用传值的方式来传递引用类型参数的行为比较相似。它会带来一定的效率提升,因为它避免了值类型实例的字段在方法调用时的拷贝操作
用传值的方式来传递引用类型参数能改变具体的引用类型对象,但不能改变作为参数传入的引用/指针,而在引用类型参数上使用out和ref关键字则可以改变传入的引用/指针,如在方法内新创建一个对象后通过参数返回指向新对象的指针,以及交换两个引用类型等
C#要求以引用方式传递的参数必须和方法期望的参数完全匹配,这与传值方法传递的参数不同。例如如果参数是Object类型,那么传入String类型的参数将无法通过编译。这样可以确保类型安全,防止一个参数经方法修改后返回其他类型的参数,如参数类型本来是Object,现在传入一个String类型的参数,但方法内部却创建了一个新的Int类型的参数并返回
9.6 可变数目参数
看下面这个方法定义:
static Int32 Add(params Int32[] values) { ... }
如果方法中没有params关键字,这个方法接受一个Int32数组引用作为参数,可以这样调用:
Int32 i = Add(new Int32[]{1, 2, 3});
有了params关键字,我们还可以这样调用:
Int32 i = Add(1, 2, 3);
params关键字告诉编译器在指定的参数上应用一个System.ParamArrayAttribute定制特性的实例。只有方法的最后一个参数才可以用params关键字,且该参数必须为一个一维数组。传递null或一个长度为0的数组给该参数是合法的,所以方法中最好对传入参数作下检测。
9.7 虚方法的调用机理
CLR在调用方法时使用了两个IL指令:call和callvirt。call指令根据引用变量的类型来调用一个方法,这也是通常调用非虚方法的方式,callvirt指令根据引用变量指向的对象类型来调用一个方法,执行时方法将会递归的调用自己直到堆栈溢出,这也就实现了运行时绑定
在用一个密封类型的引用调用虚方法时,编译器通常也会产生call指令,因为call指令不必检查引用对象的实际类型,比callvirl指令的性能要好。另外对于值类型(总是密封类型),使用call可以阻止实例被装箱。
C#也可能在调用一个引用类型的非虚方法的时候用callvirt指令,这样做是因为如果引用变量为null的时候调用callvirt会抛出System.NullReferenceException异常而call不会,而C#的语言规范要求在一个空引用上调用任何方法都应该抛出System.NullReferenceException异常
9.8 虚方法的版本问题
C#中可以用new关键字在子类型中提供与基类型中方法同名的方法,它会告诉编译器产生相关的元数据,CLR根据产生的元数据可以知道子类型中的方法应该被看作新引入的方法,和基类型中的方法没有任何关系。通常我们应该避免用new关键字让开发人员混淆,往往是基类型所属程序集更新后导致重名而此时对子类型方法名作修改代价太大才用到new
override关键字则表明子类型的方法与基类型的方法是相关联的,是对基类型虚方法的重写