大部分時候,參考型別和實值型別的區別是顯而易見的,但在用參考型別作為方法參數時,卻多次引起了我的誤解,每次都得對著電腦抓狂一下才又一次發現問題所在。
問題在於如下程式碼片段:Code Segment 1
public class Person {
public int age;
}
public static void Main(string[] args)
{
Person p = new Person(); // #1
p.age = 10;
AddAgeA(p);
Console.WriteLine(p.age);
}
public static void AddAgeA(Person p) {
p.age = 20;
}
此時會輸出20,這很明顯,現在加上了一個方法AddAgeB:public static void AddAgeB(Person p)
public static void AddAgeB(Person p) {
p = new Person(); //#2
p.age = 20;
}
當把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
public static void Main() {
int a = 5;
object obj = a;
obj = 10;
Console.WriteLine("a = " + a);
Console.WriteLine("obj = " + obj);
}
很顯然,此時輸出了 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就是實實在在的同一個引用了。