经常有人把按引用传递和引用类型混为一谈,其实是对数据存储的原理不清楚
使用规则上的不同:
ref 修饰参数的使用:传入的时候必须初始化,方法内不必重新赋值
out修饰参数的的使用:传入的时候不必初始化,但方法内必须赋值
1.C#中参数的传递方式可以分为两类,按值传递和按引用传递。如果再根据参数的类型进行细分,大致可以分为如下四种:
- 值类型的按值传递
- 按值传递时,传递过去的是该值类型实例的一个拷贝。
- 引用类型的按值传递
- 按值传递时,传递过去的是该引用类型实例的引用的一个拷贝
- 值类型的按引用传递
- 按引用传递的时候是不存在拷贝这步操作的,众所周知,值类型的实例是分配在栈上的,所以在按引用传递值类型的时候,其实是把该实例在栈上的地址(&指针),传递给了方法。
- 引用类型的按引用传递
- 引用类型的按引用传递过程,与值类型的相似,也不存在拷贝这步操作,只是将“该实例的引用”在栈上的地址(&指针),传递给了方法。
string类型作为一种特殊的引用类型,部分人认为在作为参数进行传递的时候,它的表现与其他的引用类型是不一致的。但是透过现象看本质,我们深入分析一下,就会发现,在作为参数进行传递方面,string类型与其他的引用类型是没有区别的。
交换两个字符串
按引用传递引用类型参数的一个很好的示例是交换字符串。 在示例中,str1
和 str2
两个字符串在 Main
中初始化,然后传递给 SwapStrings
方法,作为由 ref
关键字修改的参数。 这两个字符串在该方法以及 Main
内交换。
class SwappingStrings
{
static void SwapStrings(ref string s1, ref string s2)
// The string parameter is passed by reference.
// Any changes on parameters will affect the original variables.
{
string temp = s1;
s1 = s2;
s2 = temp;
System.Console.WriteLine("Inside the method: {0} {1}", s1, s2);
}
static void Main()
{
string str1 = "John";
string str2 = "Smith";
System.Console.WriteLine("Inside Main, before swapping: {0} {1}", str1, str2);
SwapStrings(ref str1, ref str2); // Passing strings by reference
System.Console.WriteLine("Inside Main, after swapping: {0} {1}", str1, str2);
}
}
/* Output:
Inside Main, before swapping: John Smith
Inside the method: Smith John
Inside Main, after swapping: Smith John
*/
在此示例中,需要按引用传递参数,以影响调用程序中的变量。 如果同时从方法标头和方法调用中删除 ref
关键字,调用程序中不会发生任何更改。
2.参数传递的是什么
按值传参传的就是值,按引用传参传的就是引用,这么简单的问题还有啥可讨论的呢。
可是想一想,值类型变量和引用类型变量组合上按值传参和按引用传参,一共四种情况,某些情况下“值”和“引用”可能指的是同一个东西。
一个变量总是和内存中的一个对象相关联。
对于值类型的变量,可以认为它总是包含两个信息,一是引用,二是对象的值。前者即是指向后者的引用。
对于引用类型的变量,可以认为它也包含两个信息,一是引用,二是另一个引用。前者仍然是指向后者的引用,而后者则指向堆中的对象。
所谓的按值传递,就是传递的“二”;按引用传递,就是传递的“一”。
也就是说,在按值传递一个引用类型的时候,传递的值的内容是一个引用。
大概情况类似于这样:
按值传递时就像是这样:
可以看到,不管方法内部对“值”和“B引用”作什么修改,两个变量包含的信息是不会有任何变化的。
但是也可以看到,方法内部是可以通过“B引用”对“引用类型对象”进行修改的。
而按引用传递时就像是这样:
可以看到,这个时候方法内部是可以通过“引用”和“A引用”直接修改变量的信息的,甚至可能发生这样的情况:
3.传递值类型参数
一个值类型变量包含它的引用类型变量相对的数据,其中包含对其数据的引用。 将值类型变量按值传递给方法,意味着将变量副本传递给该方法。 在方法内发生的对该实参进行的任何更改都不会影响存储在形参变量中的原始数据。 若要使用已调用的方法来更改参数值,必须使用 ref 或 out 关键字通过引用传递它。 还可以使用 in 关键字来按引用传递值参数,以避免复制并同时保证不更改值。 为简单起见,下面的示例使用 ref
。
3.1 按值传递值类型
下面的示例演示按值传递值-类型参数。 变量 n
按值传递给方法 SquareIt
。 在方法内发生的任何更改都不会影响该变量的原始值。
class PassingValByVal
{
static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
SquareIt(n); // Passing the variable by value.
System.Console.WriteLine("The value after calling the method: {0}", n);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 5
*/
变量 n
是值类型。 它包含其数据,值为 5
。 调用 SquareIt
时,n
的内容复制到参数 x
中,这是该方法内的平方值。 但是在 Main
中,n
的值在调用 SquareIt
方法之后与之前相同。 在方法内发生的更改只影响本地变量 x
。
3.2 按引用传递值类型
下面的示例与上述示例相同,除了自变量是作为 ref
参数传递的。 x
在方法中更改时,基础自变量的值 n
也更改。
class PassingValByRef
{
static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
SquareIt(ref n); // Passing the variable by reference.
System.Console.WriteLine("The value after calling the method: {0}", n);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 25
*/
在此示例中,它传递的不是 n
的值;而是传递 n
的引用。 参数 x
不是 int;它是对 int
的引用,在这种情况下,是对 n
的引用。 因此,当 x
在方法中进行平方计算时,实际求平方值的就是 x
所指的 n
。
3.3 交换值类型
更改自变量值的一个常见示例是交换方法,其中将两个变量传递给方法,并由该方法交换其内容。 必须通过引用将自变量传递给交换方法。 否则,交换参数在方法中的本地副本,并且调用方法中不会发生更改。 下面的示例交换整数值。
static void SwapByRef(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
调用 SwapByRef
方法时,在调用中使用 ref
关键字,如下面的示例中所示。
static void Main()
{
int i = 2, j = 3;
System.Console.WriteLine("i = {0} j = {1}" , i, j);
SwapByRef (ref i, ref j);
System.Console.WriteLine("i = {0} j = {1}" , i, j);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
/* Output:
i = 2 j = 3
i = 3 j = 2
*/
4 传递引用类型参数
引用类型的变量不直接包含其数据;它包含对其数据的引用。 如果按值传递引用类型参数,则可能更改属于所引用对象的数据,例如类成员的值。 但是,不能更改引用本身的值;例如,不能使用相同引用为新对象分配内存,并将其保留在方法外部。 为此,请使用 ref 或 out 关键字传递参数。 为简单起见,下面的示例使用 ref
。
4.1 按值传递引用类型
以下示例演示将引用类型参数 arr
按值传递给方法 Change
。 由于该参数是对 arr
的引用,因此可以更改数组元素的值。 但是,只有在方法内才能将参数重新分配给其他内存位置,且不会影响原始变量 arr
。
class PassingRefByVal
{
static void Change(int[] pArray)
{
pArray[0] = 888; // This change affects the original element.
pArray = new int[5] {-3, -1, -2, -3, -4}; // This change is local.
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
static void Main()
{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr [0]);
Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr [0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: 888
*/
在前面的示例中,数组 arr
属于引用类型,传递给不含 ref
参数的方法。 在这种情况下,将向该方法传递一个指向 arr
的引用副本。 输出表明,此方法可以更改数组元素的内容,在本示例中将 1
更改为了 888
。 但是,通过在 Change
方法内使用 new 运算符来分配一份新的内存可使变量 pArray
引用新的数组。 因此,此后的任意更改都不会影响创建于 Main
内的原始数组 arr
。 实际上,本示例创建了两个数组,一个在 Main
方法内,另一个在 Change
方法内。
4.2 按引用传递引用类型
除了 ref
关键字添加到方法参数前面,以下示例与上述示例相同。 方法中所作的任何更改都会影响调用程序中的原始变量。
class PassingRefByRef
{
static void Change(ref int[] pArray)
{
// Both of the following changes will affect the original variables:
pArray[0] = 888;
pArray = new int[5] {-3, -1, -2, -3, -4};
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
static void Main()
{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);
Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: -3
*/
方法内所作的所有更改都将影响 Main
中的原始数组。 事实上,将使用 new
运算符重新分配原始数组。 因此,调用 Change
方法后,对 arr
的任何引用都将指向 Change
方法中创建的五元素数组。
4. 3 交换两个字符串
按引用传递引用类型参数的一个很好的示例是交换字符串。 在示例中,str1
和 str2
两个字符串在 Main
中初始化,然后传递给 SwapStrings
方法,作为由 ref
关键字修改的参数。 这两个字符串在该方法以及 Main
内交换。
class SwappingStrings
{
static void SwapStrings(ref string s1, ref string s2)
// The string parameter is passed by reference.
// Any changes on parameters will affect the original variables.
{
string temp = s1;
s1 = s2;
s2 = temp;
System.Console.WriteLine("Inside the method: {0} {1}", s1, s2);
}
static void Main()
{
string str1 = "John";
string str2 = "Smith";
System.Console.WriteLine("Inside Main, before swapping: {0} {1}", str1, str2);
SwapStrings(ref str1, ref str2); // Passing strings by reference
System.Console.WriteLine("Inside Main, after swapping: {0} {1}", str1, str2);
}
}
/* Output:
Inside Main, before swapping: John Smith
Inside the method: Smith John
Inside Main, after swapping: Smith John
*/
在此示例中,需要按引用传递参数,以影响调用程序中的变量。 如果同时从方法标头和方法调用中删除 ref
关键字,调用程序中不会发生任何更改。