CLR via C#学习笔记-第九章-以传引用的方式向方法传递参数
9.3 以传引用的方式向方法传递参数
CLR默认所有方法参数都传值。传递引用类型的对象时,对象引用或者说指向对象的指针,被传给方法。
注意引用本身是传值的,意味着方法能修改对象。
对于值类型的实例,传给方法的是实例的一个副本,意味着方法获得他专用的值类型实例副本,调用者中的实例不受影响。
out和ref
CLR允许以传引用而非传值的方式传递参数。C#用关键字out或ref支持这个功能。
两个关键字都告诉C#编译器生成元数据来指明该参数是传引用的。
编译器将生成代码来传递参数的地址而非传递参数本身。
编译器对out和ref的区别对待
CLR不区分out和ref,意味着无论用哪个关键字,都会生成相同的IL代码。另外元数据也几乎完全一致,只有一个bit除外,它用于记录声明方法时指定的是out还是ref。
但C#编译器是将这个两个关键字区别对待的,而且这个区别决定了有哪个方法负责初始化所引用的对象。
使用out标记的方法参数,表明不指望调用者在调用方法之前初始化好了对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。
相反,如果方法的参数用ref来标记,调用者就必须在调用该方法前必须向这个值写入,被调用的方法可以读取值或者向值写入。
值类型使用out和ref
对于out和ref,引用类型和值类型的行为迥然有异。首先看一看值类型使用out和ref
public sealed class Program { public static void Main(string[] args) { Int32 x;//x没有初始化 GetVal(out x);//x不必初始化 Console.WriteLine(x); } private static void GetVal(out Int32 v) { v=10;//该方法必须初始化v } }
上述代码中,x在Main的栈帧中声明。然后x的地址传给GetVal。
GetVal的v是一个指针,指向Main栈帧中的Int32值。
在GetVal内部,v指向的那个Int32被更改为10。GetVal返回时Main的x就有了一个为10的值。
为大的值类型使用out,可以提升代码的执行效率,因为它避免了在进行方法调用时复制值类型实例的字段。
下例用ref代码代替了out。
public sealed class Program { public static void Main(string[] args) { Int32 x=5;//x已经初始化 GetVal(ref x);//x必须初始化 Console.WriteLine(x); } private static void AddVal(out Int32 v) { v+=10;//该方法可以使用v的已经初始化的值 } }
在上述代码中,x也在Main的栈帧中声明,并初始化为5。
然后x的地址传给AddVal。AddVal的v是一个指针,指向Main栈帧中的Int32值。
在AddVal内部,v指向的那个Int32要求必须是已经初始化的。
因此,AddVal可在任何表达式中使用该初始值。
AddVal还可以更改这个值,新值会返回调用者。
本例中,AddVal将10加到初始值上。AddVal返回时,Main的x将包含15。
综上所述,从IL和CLR的角度看,out和ref是同一码事:都导致传递指向实例的一个指针。但从编译的角度看,二者是有区别的。
注释
栈帧stack frame代表当前线程的调用栈中的一个方法调用,在执行线程的过程中进行的每个方法调用都会在调用栈中创建并压入一个stackframe
out/ref的参数可以重载方法
C#要求必须在调用方法时指定out或ref。C#设计者认为调用者应该显式表明意图,即使编译器能自动采取正确操作。
CLR允许根据使用的是out还是ref参数对方法进行重载。
public sealed class Point { static void Add(Point P){} static void Add(ref Point p){} }
两个重载方法只有out和ref的区别则不合法,因为两个签名的元数据形式完全相同。所以不能在上述Point类中在定义以下方法
static void Add(out Point p){}
对值类型和引用类型使用out/ref时的内存操作
为值类型使用out和ref,效果等同于以传值的方式传递引用类型。
对于值类型,out和ref允许方法操纵单一的值类型实例。调用者必须为实例分配内存,被调用者则操纵该内存中的内容。
对于引用类型,调用代码为一个指针分配内存,该指针指向一个引用类型的对象,被调用者则操纵这个指针。正因为如此,仅当方法返回对方法知道的一个对象的引用时,为引用类型使用out和ref才有意义。
传递的参数必须与方法预期的参数匹配,原因是保障类型安全。可以使用泛型来解决这一问题。