CLR默认所有方法都传值。传递引用类型的对象时,对象引用(或者说指向对象的指针)被传递给方法。注意引用(或指针)本身是传值的,意味着方法能修改对象,而调用者能看到这些修改。对于值类型的实例,传给方法的是实例的一个副本,意味着方法将获得它专用的一个值类型的实例副本,调用者中的实例不受影响。
重要提示:在方法中,必须知道传递的每个参数是引用类型还是值类型,处理参数的代码显著有别。
CLR允许以传引用而非传值的方式传递参数。C#用关键字out或ref支持这个功能。两个关键字都告诉C#编译器生成元数据来指明该参数是传引用的。编译器将生成代码来传递参数的地址,而非传递参数本身。
CLR不区分out和ref,意味着无论用哪个关键字,都会生成相同的IL代码。另外,元数据也几乎完全一致,只有一个bit除外,它用于记录声明方法时指定的是out还是ref。但C#编译器是将这两个关键字区别对待的,而且这个区别决定了由哪个方法负责初始化所引用的对象。如果方法的参数用out来标记,表明不指望调用者在调用方法之前初始化好了对象。被调用的方法不能读取参数的值,而且在返回前必须向这个值写入。相反,如果方法的参数用ref来标记,调用者就必须在调用该方法前初始化参数的值,被调用的方法可以读取值以及/或者向值写入。
//--
在上述代码中,x在Method的栈帧中声明。然后,x的地址传递给GetVal。GetVal的v是一个指针,指向Method栈帧中的int值。在GetVal内部,v指向的那个int被更改为10。GetVal返回时,Method的x就有了一个为10的值。
为大的值类型使用out,可提升代码的执行效率,因为它避免了在进行方法调用时复制值类型实例的字段。
//--
在上述代码中,x也在Method的栈帧中声明,并初始化为5。然后,x的地址传给AddVal。AddVal的v是一个指针,指向Method栈帧中的int值。在AddVal内部,v指向的那个int要求必须是已经初始化的。因此,AddVal可在任何表达式中使用该初始值。AddVal还可更改这个值,新值会“返回”调用者。
//--
out
ref
综上所述,从IL和CLR的角度看,out和ref是同一码事:都导致传递指向实例的一个指针。但从编译器的角度看,两者是有区别的。根据市out还是ref,编译器会按照不同的标准类验证你写的代码是否正确。
//--
C#要求必须在调用方法时指定out或ref。因为语言设计者认为调用者应显式表明意图。
另外,CLR允许根据使用out还是ref参数对方法进行重载。在C#中是合法的。
但是两个重载方法只有out和ref的区别则不合法,因为两个签名的元数据形式完全相同。
//--
下面展示如何用ref关键字实现一个用于交换两个引用类型的方法:
为了交换两个string你或许以为可以这样:
但是无法通过编译。问题在于,对于以传引用的方式传给方法的变量,它的类型必须是与方法签名中声明的类型相同。
你可以这么写:
传递的参数之所以必须与方法预期的参数匹配,原因是保障类型安全。
//--
可以使用泛型类修正这些方法:
完美运行。