小記一次讓我誤會的引用傳參
問題在于如下代碼段:
此時會輸出20,這很明顯,現在加上了一個方法AddAgeB:
當把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。
再回頭看看裝箱:
很顯然,此時輸出了 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就是實實在在的同一個引用了。



浙公網安備 33010602011771號