你真的了解C#中的值和引用吗?(下)

前两天讨论了一下关于值类型存储位置常见的误区,没有想到我认为尽人皆知的秘密还是有人心存疑问。虽然我也不能举出有力的证据证明这一点(引用类型的值类型字段存储在堆上),但实际上这属于实现细节。我上一篇文章想重点强调的就是,不能把实现细节当真理,因为它是不稳定的。

今天要讨论的话题是参数传递,这不是实现细节。

参数的种类

C#中的参数共分为4种:

  • 值参数(按值传递的参数)
  • 引用参数(按引用传递的参数,使用ref修饰符)
  • 输出参数(使用out修饰符)
  • 参数数组(使用params修饰符)

本文主要讨论参数按值传递和按引用传递的区别,以及值类型和引用类型在按值传递和按引用传递时的表现。我们均以向方法传递参数为例。

按值传递的参数

C#的参数在默认情况下都是按值传递的。也就是说,当向方法传递参数的时候,会创建一个新的存储位置,然后将参数的值复制一份放到该存储位置中。相当于声明了一个局部变量(实参),然后用传入的参数的值初始化这个变量。如果在方法内改变实参的值,将不会影响到方法调用的上下文。

上篇文章提到过,值类型表达式的值是数据本身,引用类型表达式的值是到对象的引用。所以,按值传递的时候,对值类型的参数来说,复制的是该值类型参数所存储的数据;对引用类型的参数来说,复制的值是到具体对象的引用。(注意,这和直接用变量对另一个变量赋值是一样的。)

值类型按值传递

注意如刚才所说,值类型按值传递时,将复制该值类型本身所代表的数据。如下面的代码片段:

// #Code1
int i = 5;
N(i);
Console.WriteLine(i);
...
void N(int j)
{
    j = 10;
}

虽然在N内部,j被设置为10,但实际上在方法调用的上下文中,i的值仍然是5,改变的是i的一个副本j。如下图所示:

image

引用类型按值传递

对引用类型的参数来说,复制的值是到具体对象的引用。如下面的代码片段:

// #Code2
StringBuilder sb1 = new StringBuilder("Hello");
M(sb1);
Console.WriteLine(sb1);
...
void M(StringBuilder sb2)
{
    sb2 = null;
}

虽然在M内部,sb2被设置为null,但实际的输出结果仍然是“Hello”。如下图所示:

image

注意,我们说的是在方法内部改变实参的,不会对外部调用上下文产生影响。对于引用类型的实参,如果在方法内部更改所引用的对象的数据,实参和外部变量仍然引用的是同样的对象,因此都会受到影响。例如下面的代码:

// #Code3
StringBuilder sb1 = new StringBuilder("Hello");
M(sb1);
Console.WriteLine(sb1);
...
void M(StringBuilder sb2)
{
    sb2.Append(" world");
}

输出结果将为“Hello world”。如下图所示:

image

按引用传递的参数

按引用传递不会涉及隐式复制。它所传递的,不是在调用方法时传递给方法的变量的值,而是变量本身。它不会创建新的存储位置,而是使用与变量相同的存储位置,因此,在调用方法上下文中传递给方法的变量与方法内部使用的参数,实际上是同一个。

在按引用传递参数时,在方法的声明和调用的地方都必须显式使用ref修饰符,这是为了让你清楚你正在进行的是与默认传递方式不同的按引用传递。

值类型按引用传递

按照引用传递的定义,我们实际上是把变量本身传递给了方法,在方法内和调用方法的地方,使用的实际上是同一个变量。因此对于值类型来说,在方法内部对参数所做的任何改动,也都会反映到方法外部。#Code1改成按引用传递后,输出结果将为10。

// #Code4
int i = 5;
N(ref i);
Console.WriteLine(i);
...
void N(ref int j)
{
    j = 10;
}

参数的传递过程如下图所示:

image

引用类型按引用传递

应用类型按应用传递与值类型按引用传递表现形式是一样的,在方法内部所做的任何改变,都将反映到外部变量上。因此,如#Code2将sb2设置为null,则外部的sb1也会变成null,如#Code3调用sb2.Append,外部的sb1也会进行相应的改变。

// #Code5
StringBuilder sb1 = new StringBuilder("Hello");
M(ref sb1);
Console.WriteLine(sb1);
...
void M(ref StringBuilder sb2)
{
    sb2 = null;
}

如图所示:

image

结论

我们通常提到C#中的值和引用,大多数情况可能都是指值类型和引用类型,但实际上值和引用有着更加丰富的含义。我这两篇文章试图把这些概念总结出来,讲解了值类型和引用类型的值是什么,以及它们在按值传递和按参数传递时有什么相同和不同之处。更重要的是,关于值类型和引用类型,我们平时在认识上存在很多误区,把相关的实现细节作为知识点记忆了下来。其实这些只是其然,不是所以然。而所以然,我们也没必要去深究。

很多面试官在考察面试者时,也会不自觉得去问一些实现细节,这完全没有必要。(当然,很可能面试官也陷入了这样的误区。)以后我们应该牢记的就是,当提到引用类型和值类型区别的时候,如果有人扯到存储位置这个话题上来(90%的人都大概会这样吧),你应该把他们引导到正确的方向上来。

参考资料

相关文章

posted @ 2012-06-15 16:55 麒麟.NET 阅读(...) 评论(...) 编辑 收藏