小记一次让我误会的引用传参

大部分时候,引用类型和值类型的区别是显而易见的,但在用引用类型作为方法参数时,却多次引起了我的误解,每次都得对着电脑抓狂一下才又一次发现问题所在。

问题在于如下代码段:
Code Segment 1

此时会输出20,这很明显,现在加上了一个方法AddAgeB:
public static void AddAgeB(Person p)


当把Main方法中的AddAgeA(p)改成AddAgeB(p)后,就输出了10,而不再是20。

于是我自言自语:不是说class都是引用类型么?AddAgeB一样是接收一个引用类型Person,虽然方法体中新new了一个Person,但是紧跟在后面的一句一样对这个引用类型的age字段赋值了啊?
在Main方法中Person p = new Person() (#1处)时,在堆中就分配了一块空间,下一句向这块空间中的age所在的内存空间存入了10,然后把p这个引用传递给AddAgeB()方法,在AddAgeB()中又在堆中开辟了一块新的空间(#2处),然后把新开辟的内存地址赋给p,下一句把新空间中的age赋为20,这么说来,Main()中的输出应该变成20了啊?难道这样不对么?

我抓狂了一下后发现,上面我的自言自语中缺失了一个很重要的细节,而这个细节,就是关键所在。问题出在这一句:“然后把p这个引用传递给AddAgeB()方法”,这太含糊了,我忘记了这里的“引用传递”其实是一个“复制”的过程。在上面两个AddAge方法体中的p,其实只是Main方法中的p的一个副本,当执行:Person p = new Person() 时,会在托管堆上开辟一块地址空间,存放Person的一个实例,这个实例的age字段就存在这个空间里头,同时,在栈上还会分配一块内存空间,它用来存放Person实例所在的堆内存地址,而p的值,就是栈上的这个地址。当执行 AddAgeA(p) 或 AddAgeB(p)时,栈上p的值(内存地址)会被复制,而在AddAge方法体中所使用的p,就是这个复制出来的副本,而不是原来的p,但是,它们的值(地址)是一样的,都是指向托管堆中的Person实例地址,所以,在AddAgeA()中,当执行p.age = 20时,托管堆中的那个Person实例的age字段的值就变成了20,于是,在Main方法中输出时,因为Main方法中的p也是指向这块堆空间地址,所以输出了20。

而在AddAgeB()中,有关键的一句:p = new Person(),这一句会在托管堆中再开辟一块新的内存空间,把后把它的地址赋给栈上的p,在这个时候,Main方法中的p和AddAgeB方法中的p不仅在栈上的位置不一样,值也不一样,当在执行下一句:p.age = 20;时,新开辟的堆空间中的age的值的的确确是改变了,但是在出了方法体的右花括号之后,方法体中的p(在栈上)就因为作用域的原因而被释放,那么,AddAgeB方法中创建的那个Person实例就成了一个等待GC来回收的垃圾,因为此时堆中存在了两个Person实例,在Main方法中的那个p所指向的堆空间中的age并没有被改变,所以在Main方法中输出了10。

再回头看看装箱:

Box Main

很显然,此时输出了 a = 5 , obj = 10。
在第二行中,有一个装箱操作,a的值为被“复制”一份到obj中,所以a的值不变,obj的值在执行obj = 10之后变了。

现在改一下:
public static void Main() {
    
int a = 5;
    
object obj = a;
    Change(obj);
    Console.WriteLine(
"a = " + a);
    Console.WriteLine(
"obj = " + obj);
}

public void Change(object obj) {
    obj 
= 10;
}

此时会输出什么呢?
正确答案是a = 5, obj = 5

由此也可得知,当在Change方法体中执行obj = 10时,10会被装到托管堆中的箱子里面去,然后obj的值被重新赋为这个箱子所在的起始地址,由于方法体中的obj引用的值跟Main方法中的obj引用的值并不一样,所以,输出的obj还是5。

总结一下,其实就是几个字:

如果没有用ref或out关键字的话,在给方法传递参数时,会有一个复制的过程,不管是值类型还是引用类型。


当然,用上了ref的话,就没有了这个复制过程,Main方法中的p和AddAge方法中的p就是实实在在的同一个引用了。
posted @ 2008-06-18 23:23  水言木  阅读(314)  评论(0编辑  收藏  举报