小記一次讓我誤會的引用傳參

來源:互聯網
上載者:User
大部分時候,參考型別和實值型別的區別是顯而易見的,但在用參考型別作為方法參數時,卻多次引起了我的誤解,每次都得對著電腦抓狂一下才又一次發現問題所在。

問題在於如下程式碼片段: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就是實實在在的同一個引用了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.