在C#中,既可以通过值也可以通过引用传递参数。
总的来说,通常我们向方法中传递的是值。方法获得的是这些值的一个拷贝,然后使用这些拷贝,当方法运行完毕后,这些拷贝将被丢弃,而原来的值不将受到影响。然而,通过引用传递参数允许函数成员更改参数的值,并保持该更改。
当我们需要改变原来变量中的值,这时,我们可以向方法传递变量的引用,而不是变量的值。引用是一个变量,他可以访问原来变量的值,修改引用将修改原来变量的值。变量的值存储在内存中,可以创建一个引用,他指向变量在内存中的位置。当引用被修改时,修改的是内存中的值,因此变量的值可以将被修改。当我们调用一个含有引用参数的方法时,引用参数的修改对应着该引用所指向内存值的更改,因此,我们会明白,为什么当修改参数变量时,也将导致原来变量的值的改变。
实际上从名称上我们可以清楚地看出两者的含义——传值参数传递的是调用参数的一份拷贝,而传址参数传递的是调用参数的内存地址,该参数在方法内外指向的是同一个存储位置。
若要通过引用传递参数,可使用ref或out关键字。ref和out这两个关键字都能够提供相似的功效,其作用也很像C中的指针变量。下面主要介绍这两个关键字的区别。
注:在C#中,方法的参数传递有四种类型:传值(by value),传址(by reference),输出参数(by output),数组参数(by array)。传值参数无需额外的修饰符,传址参数需要修饰符ref,输出参数需要修饰符out,数组参数需要修饰符params。
MSDN中的定义:
ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字。
out 关键字会导致参数通过引用来传递。这与 ref 关键字类似,不同之处在于 ref 要求变量必须在传递之前进行初始化。若要使用 out 参数,方法定义和调用方法都必须显式使用 out 关键字。
首先,我们来看一个简单的例子: static void TestRefAndOut()
{
string s1 = "Good Luck!";//变量必须在传递之前进行初始化
TestRef(ref s1);
Console.WriteLine(s1);//output: Hello World!
}
static void TestRef(ref string str)
{
str = "Hello World!";
}
在TestRefAndOut()中,将字符串s1以ref关键字的方式传到方法TestRef(ref string str)中,在这个方法中,我们改变了s1的引用变量str的值,最后,回到TestRefAndOut()方法后输出s1的值,发现其值已被改变。
将上例中的ref换成out,代码如下:
static void TestRefAndOut()
{
string s1 = "Good Luck!";
//TestRef(ref s1);
TestOut(out s1);
Console.WriteLine(s1);//output: Hello World!
}
static void TestOut(out string str)
{
str = "Hello World!";//在方法进入后,必须赋值
}
同样,在将ref换成out后,会发现最后的输出仍然是相同的,那这两个关键字的区别是什么呢?
进一步测试: ref:
static void TestRefAndOut()
{
string s1 = "Good Luck!";//变量必须在传递之前进行初始化
TestRef(ref s1);
}
static void TestRef(ref string str)
{
Console.WriteLine(str);//output: Good Lick!
}
out:
static void TestRefAndOut()
{
string s1 = "Good Luck!";
TestOut(out s1);
}
static void TestOut(out string str)
{
Console.WriteLine(str);//compile does not pass
}
ref的那段代码顺利编译,输出"Good Luck!",而out那段代码却无法通过编译,提示“Use of unassigned out parameter 'str' ”,即使用了未分配地址的out参数str。怎么回事呢?原来 out参数在进入方法的时候,C#会自动清空它的一切引用和指向,所以在上面的out例子中, 必需先要为str参数赋值。如以下程序。
static void TestRefAndOut()
{
string s1 = "Good Luck!";
TestOut(out s1);
}
static void TestOut(out string str)
{
str = "Hello World!";//因为原来str的引用和指向已经清空,所以,这里必需先要为str参数赋值
Console.WriteLine(str);//output: Hello World!
}
Ok,得到第一个区别: out 参数在进入方法(函数)后清空自己,使自己变成一个干净的参数,因此,在方法返回之前或再使用out参数前,必须为 out 参数赋值(只有地址没有值的参数是不能被.net接受的);而在被调用方法中,ref参数是不必在使用前先赋值的,甚至也可以被调用方法中不改变ref参数的值,这都不会引起编译错误。
再继续看一段代码: ref:
static void TestRefAndOut()
{
string s1;
TestRef(ref s1);//没有为s1赋值,即开始的引用为空
Console.WriteLine(s1);//compile does not pass!
}
static void TestRef(ref string str)
{
str = Hello World!";//在方法在为str赋值,无效
}
out:
static void TestRefAndOut()
{
string s1;
TestOut(out s1);//同样没有为s1赋值
Console.WriteLine(s1);//output: Hello World!
}
static void TestOut(out string str)
{
str = "Hello World!";//在方法中为str赋值,有效
}
这回发现,ref这段代码无法编译了,s1是一个空引用,所以无法使用。而out参数则因为上述的那个原因,它不在乎s1是不是空引用,因为就算s1不是空引用,它也会把s1变成空引用的。
Ok,第二个区别:ref参数在使用前必需初始化,而out不需要。嗯,由上边两个区别可以引申一下,out参数只出不进,ref参数有进有出。
在用法上:out适合用在需要retrun多个返回值的地方,而ref则用在,需要被调用的方法( TestRef(ref string str)),修改调用者(TestRefAndOut())的引用的时候。下面用两个实例说明。
ref有进有出,out只进不出示例代码:
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
//ref是有进有出,而out是只出不进,
public int RefValue(int i, ref int j)
{
int k = j; //表现为有进,将j在外部的初始值传进来
j = 222; //表现为有出,j是返回值
return i + k;
}
public int OutValue(int i, out int j)
{
j = 222;//表现为有出,在方法内部赋值,并作为out的返回值输出
return i + j;
}
private void btn_ref_Click(object sender, EventArgs e)
{
int m = 0;
MessageBox.Show(RefValue(1, ref m).ToString());//返回值为1
MessageBox.Show(m.ToString());//返回值是222,说明refvalue的调用ref是传址
}
private void btn_out_Click(object sender, EventArgs e)
{
int m;
MessageBox.Show(OutValue(1, out m).ToString());//返回223
MessageBox.Show(m.ToString());//返回222
}
}
out适用于多个返回值示例:
class gump
{
public void math_routines(double x,out double half,out double squared,out double cubed)
{
half=x/2; //输出的值需要靠x赋值,x不能再为输出值
squared=x*x;//x为赋值,所以可以是double 类型的变量,也可以是ref 标识的变量,但是不能是out输出类型的
cubed=x*x*x;
}
}
class TestApp
{
public static void calc()
{
gump doit=new gump();
double x1=600;
double half1=0;
double squared1=0;
double cubed1=0;
/*
double x1=600;
double half1;
double squared1;
double cubed1;
*/
Console.WriteLine("Before method->x1={0}",x1);//600
Console.WriteLine("half1={0}",half1); //0
Console.WriteLine("squared1={0}", squared1); //0
Console.WriteLine("cubed1={0}", cubed1); //0
doit.math_routines(x1, out half1, out squared1, out cubed1);//该函数调用前后,half1,squared1,cubed1的值发生了变化
Console.WriteLine("After method->x1={0}",x1);//600
Console.WriteLine("half1={0}",half1); //300
Console.WriteLine("squared1={0}",squared1);//3600
Console.WriteLine("cubed1={0}",cubed1);//2160000
}
}
我们发现,ref和out似乎可以实现相同的功能。因为都可以改变传递到方法中的变量的值。但是,二者本质本质的区别就是,ref即可以是传入值又可以是传出值,out只是传出值。在含有out关键字的方法中,变量必须由方法参数中不含out(可以是ref)的变量赋值或者由全局(即方法可以使用的该方法外部变量)变量赋值,out的宗旨是保证每一个传出变量都必须被赋值。
上面代码中被/**/注释掉的部分,可以直接使用。也就是说,在调用方法前可以不初始化变量.但是\"x1=600\"是要赋值的,否则要报错.而ref参数,在传递给方法时,就已经是还有值的了,所以ref侧重修改,out侧重输出。
总结:
1、使用ref型参数时,传入的参数必须先被初始化。对out而言,必须在方法中对其完成初始化。
2、使用ref和out时,在方法的定义和执行时,都要加Ref或Out关键字。以满足匹配。
3、out适合用在需要retrun多个返回值的地方,而ref则用在需要被调用的方法修改调用者的引用时。
4、ref有进有出,out只进不出。