一个方法可以包括 0 或多个参数。在方法前面括号中规定的参数列表称为形参,而传递进方法的参数称为实参。
C# 的默认方式是按值传递(pass by value),若传递对象是值类型,则按值传递之后,传递进方法的不过是值的副本而已,方法外部的对象不受影响。
按引用传递(pass by reference)之后,传递进方法的是值类型的地址,方法外部的对象会受影响。
实际上,按引用传递是按值传递的一种特殊情况(引用是地址,地址也是值)。
例如一个试图调换 a 和 b 值的 Swap 函数,如果其签名为 void Swap(int a, int b),则如果在某处调用该函数,调用完毕后,a 和 b 的值是不会变的。
因为,传入时会自动复制两个 int 副本,函数内部的任何操作和 a,b 都没有关系。
如果其签名是 void Swap(ref int a, ref int b),,则改为传入 a 和 b 的地址,不会发生复制,调用该函数将确实会调换 a 和 b 的值。
如果传递对象是引用类型,则无论是普通的传递还是加上 ref 和 out 关键字,都会更改方法外部的对象。
加不加 ref 和 out 关键字的区别在于是否可以把引用重新赋值给一个新的对象(即更改传入的地址本身)。
ref 和 out 的区别是,ref 需要事先赋值,而 out 必须在方法返回之前赋值。
另外,一个方法可以有多个由 ref 或 out 修饰的输入变量,这间接地令方法有了多个返回值。
如果两个方法仅仅在 ref 和 out 上有区别(相同名称,相同输岀,相同输入),则在编译器看来他们是相同的方法。
特殊情况:如果传递的对象是字符串,则其行为类似值类型(字符串的值不改变)。要 传递引用,必须要加 ref 关键字才可以。
例如 void Swap(string a, string b),调用完毕后,外 部字符串是不会被交换的。
为了验证引用类型的按引用传递,我们来看下面的代码:
public class AClass
{
public int a;
public string b;
public static void ChangeValue(AClass a)
{
a.a = 111;
var b = new AClass();
b.a = 222;
a = b;
}
public static void ChangeValueRef(ref AClass a)
{
a.a = 888;
var b = new AClass();
b.a = 999;
a = b;
}
}
当我们调用时:
static void Main(string[] args)
{
var a = new AClass();
a.a = 1;
AClass.ChangeValue(a);
Console.WriteLine(a.a);
AClass.ChangeValueRef(ref a);
Console.WriteLine(a.a);
Console.ReadKey();
}
会输出 111 和 999。我们发现,按值传递时,引用类型栈上的地址不能改变;而按引用传递时,可以改变栈上的地址,指向一个新的堆上的对象。
在看完上面的例子之后,理解字符串的行为可能就容易了一些。看下面的代码:
static void Main(string[] args)
{
var s = "test";
Change(s);
Console.WriteLine(s);
Console.WriteLine(Change2(s));
ChangeRef(res s);
COnsole.ReadKey();
}
public static void Change(string s)
{
s += "1";
}
public static void Change2(string s)
{
return s += "2";
}
public static void ChangeRef(ref string s)
{
s += "3";
}
当调用 Change 方法之后,字符串池中有两个字符串,分别为 test 和 test 1(大部分情况下,字符串操作每次都产生新的字符串)。
但是,由于没有更改 s 本身引用的指向(令 s 等于它),所以 test1 实际上没有引用指向它,方法结束之后,它就变成了垃圾。
此时,外部的 s 仍然指向 test,所以,s 的值不发生变化。这和普通的引用类型按值传递不同,因为,普通的引用类型没有不变性,不会在操作时产生新的对象。
Change2 方法则返回一个值,所以打印岀的值发生了变化,s 现在指向一个新的值。此时,内存中又增加了一个字符串 test2。
Change3 方法传递引用,所以可以把字符串 s 赋值给一个新的对象 test3。这和普通的引用类型按引用传递相同。