在默认的情况下,CLR假定所有的方法的参数都是按值传递的。对于值类型的实例,传递给方法的将是实例的副本,这意味着方法会得到一份属于自己的值类型实例的副本,调用者中的实例不会受到任何影响。
在C#中,使用out和ref这两个关键字表明参数是按引用传递的,从CLR的角度来看,这两个关键字是等效的,也就是说无论使用哪个都会生成相同的元数据和IL代码,但是C#编译器将这两个关键字是区别对待的,它们的主要区别在于哪个方法负责初始化对象,如果用out,那么调用者不希望在在调用方法之前初始化对象,被调用的方法不能读取对象的值,而且被调用的方法必须在返回之前为对象赋值,如果用ref,那么调用者必须在调用方法之前初始化对象,被调用的方法可以读取参数或者为参数赋值。
注意,使用out和ref这两个关键字时,在声明和调用处都要加了这两个关键字。看下面代码:
using System;
public sealed class Program
{
public static void Main()
{
Int32 x ; //x还没有被初始化
GetVal(out x); //x不必被初始化,注意要加上out
Console.WriteLine(x.ToString()); //显示10
}
//声明该方法要加了out
private static void GetVal(out Int32 v)
{
v = 10; //该方法必须初始化v
}
}
我们把了面代码给改一下:
using System;
public sealed class Program
{
public static void Main()
{
Int32 x = 5; //x已经被初始化
GetVal(out x);
Console.WriteLine(x.ToString()); //显示10
}
//声明该方法要加了out
private static void GetVal(out Int32 v)
{
Console.WriteLine(v.ToString()); //编译不通过,使用了未赋值的out参数v
v = 10; //该方法必须初始化v
Console.WriteLine(v.ToString());//显示10
}
}
上述代码中,x是在Main()方法的堆栈框架中声明的,然后x的地址传递给GetVal()。GetVal()的参数v是一个指向Main()堆栈框架中的值类型为Int32指针。在GetVal()中,v指向的Int32类型的值被改为10,当GetVal()返回时,Main()方法中的x的值为10。对大量值类型使用关键字out后,代码效率会提高,因为关键字out在进行方法调用时可以避免复制值类型字段的实例。
再看下面用ref的代码:
using System;
public sealed class Program
{
public static void Main()
{
Int32 x = 5; //x已经被初始化
AddVal(ref x); //x必须被初始化
Console.WriteLine(x.ToString()); //显示15
}
//声明该方法要加上ref
private static void AddVal(ref Int32 v)
{
v = v + 10; //该方法可以使用v中已经实例化过的值
}
}
上述代码中,x也是在Main()方法的堆栈框架中声明的,而且x的值被初始化为5,然后x的地址传递给AddVal()。AddVal()的参数v是一个指向Main()堆栈框架中的值类型为Int32指针。在AddVal()中,v指向的Int32类型的值要求事先拥有值,因此,AddVal可以在任意表达式中使用该初始值。AddVal同样也可以改变值,而且新值将“返回”给调用代码。
从IL和CLR的角度来看,out和ref功能相同,它们都生成一个所传递实例的指针。
CLR还允许基于out和ref参数的方法重载,例如:
public sealed class Point
{
static void Add(Point p) { }
static void Add(ref Point p) { }
}
但不能同时存在out和ref的重载,所以上面方法要是再加上
static void Add(out Point p) { }
编译将不能通过。
下面的例子演示了如何使用ref关键字来实现用于交换两个引用类型的方法:
public static void Swap(ref Object a, ref Object b)
{
Object t = b;
b = a;
a = t;
}
为了交换两个String对象的引用,我们可以这样实现:
public static void SomeMethod()
{
String s1 = "string1";
String s2 = "string2";
Swap(s1, s2);
Console.WriteLine(s1);
Console.WriteLine(s2);
}
但是,上面代码并不能通过编译,因为按引用传递给方法的变量的类型必须与方法签名中声明的类型相同,也就是上面的Swap方法需要两个Object的引用,而不是String引用,所以我们可以像下面这样实现:
public static void SomeMethod()
{
String s1 = "string1";
String s2 = "string2";
Object o1 = s1, o2 = s2;
Swap(ref o1,ref o2);
s1 = (String)o1;
s2 = (String)o2;
Console.WriteLine(s1);
Console.WriteLine(s2);
}
可以使用泛型来修改这些方法,如下所示:
public static void Swap<T>(ref T a, ref T b)
{
T t = b;
b = a;
a = t;
}
public static void SomeMethod()
{
String s1 = "string1";
String s2 = "string2";
Swap(ref s1,ref s2);
Console.WriteLine(s1);
Console.WriteLine(s2);
}