深刻理解C#的傳值調用和傳引用調用

傳值調用和傳引用調用是幾乎所有主流語言都會涉及到的問題,下面我談談我對C#中傳值調用和傳引用調用的理解。

1. 一般對C#中傳值調用和傳引用調用的理解

  • 如果傳遞的參數是基元類型(int,float等)或結構體(struct),那麼就是傳值調用。
  • 如果傳遞的參數是類(class)那麼就是傳引用調用。
  • 如果傳遞的參數前有ref或者out關鍵字,那麼就是傳引用調用。

驗證示例的代碼如下:

using System; 
  
public class ArgsByRefOrValue 
{ 
    public static void Main(string[] args) 
    { 
        // 實驗1. 傳值調用--基元類型 
        int i = 10; 
        Console.WriteLine("before call ChangeByInt: i = " + i.ToString()); 
        ChangeByInt(i); 
        Console.WriteLine("after call ChangeByInt: i = " + i.ToString()); 
  
        Console.WriteLine("=============================================="); 
        // 實驗2. 傳值調用--結構體 
        Person_val p_val = new Person_val(); 
        p_val.name = "old val name"; 
        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name); 
        ChangeByStruct(p_val); 
        Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗3. 傳引用調用--類 
        Person_ref p_ref = new Person_ref(); 
        p_ref.name = "old ref name"; 
        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name); 
        ChangeByClass(p_ref); 
        Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗4. 傳引用調用--利用ref 
        Person_ref p = new Person_ref(); 
        p.name = "old ref name"; 
        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name); 
        ChangeByClassRef(ref p); 
        Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name); 
  
        Console.ReadKey(true); 
    } 
  
    static void ChangeByInt(int i) 
    { 
        i = i + 10; 
        Console.WriteLine("when calling ChangeByInt: i = " + i.ToString()); 
    } 
  
    static void ChangeByStruct(Person_val p_val) 
    { 
        p_val.name = "new val name"; 
        Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name); 
    } 
  
    static void ChangeByClass(Person_ref p_ref) 
    { 
        p_ref.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name); 
    } 
  
    static void ChangeByClassRef(ref Person_ref p) 
    { 
        p.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name); 
    } 
} 
  
public struct Person_val 
{ 
    public string name; 
} 
  
public class Person_ref 
{ 
    public string name; 
}


 

運行結果如下:

看起來似乎上面代碼中實驗3實驗4是一樣的,即對於類(class)來說,不管加不加ref或out,都是傳引用調用。

其實,這只是表面的現象,只要稍微改一下代碼,結果就不一樣了。

修改上面代碼,再增加兩個實驗。

using System; 
  
public class ArgsByRefOrValue 
{ 
    public static void Main(string[] args) 
    { 
        // 實驗1. 傳值調用--基元類型 
        int i = 10; 
        Console.WriteLine("before call ChangeByInt: i = " + i.ToString()); 
        ChangeByInt(i); 
        Console.WriteLine("after call ChangeByInt: i = " + i.ToString()); 
  
        Console.WriteLine("=============================================="); 
        // 實驗2. 傳值調用--結構體 
        Person_val p_val = new Person_val(); 
        p_val.name = "old val name"; 
        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name); 
        ChangeByStruct(p_val); 
        Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗3. 傳引用調用--類 
        Person_ref p_ref = new Person_ref(); 
        p_ref.name = "old ref name"; 
        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name); 
        ChangeByClass(p_ref); 
        Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗4. 傳引用調用--利用ref 
        Person_ref p = new Person_ref(); 
        p.name = "old ref name"; 
        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name); 
        ChangeByClassRef(ref p); 
        Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗5. 傳引用調用--類 在調用的函數重新new一個對象 
        Person_ref p_ref_new = new Person_ref(); 
        p_ref_new.name = "old new ref name"; 
        Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 
        ChangeByClassNew(p_ref_new); 
        Console.WriteLine("after call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 
  
        Console.WriteLine("=============================================="); 
        // 實驗6. 傳引用調用--利用ref 在調用的函數重新new一個對象 
        Person_ref p_new = new Person_ref(); 
        p_new.name = "old new ref name"; 
        Console.WriteLine("before call ChangeByClassRefNew: p_new.name = " + p_new.name); 
        ChangeByClassRefNew(ref p_new); 
        Console.WriteLine("after call ChangeByClassRefNew: p_new.name = " + p_new.name); 
  
        Console.ReadKey(true); 
    } 
  
    static void ChangeByInt(int i) 
    { 
        i = i + 10; 
        Console.WriteLine("when calling ChangeByInt: i = " + i.ToString()); 
    } 
  
    static void ChangeByStruct(Person_val p_val) 
    { 
        p_val.name = "new val name"; 
        Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name); 
    } 
  
    static void ChangeByClass(Person_ref p_ref) 
    { 
        p_ref.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name); 
    } 
  
    static void ChangeByClassRef(ref Person_ref p) 
    { 
        p.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name); 
    } 
  
    static void ChangeByClassNew(Person_ref p_ref_new) 
    { 
        p_ref_new = new Person_ref(); 
        p_ref_new.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClassNew: p_ref_new.name = " + p_ref_new.name); 
    } 
  
    static void ChangeByClassRefNew(ref Person_ref p_new) 
    { 
        p_new = new Person_ref(); 
        p_new.name = "new ref name"; 
        Console.WriteLine("when calling ChangeByClassRefNew: p_new.name = " + p_new.name); 
    } 
} 
  
public struct Person_val 
{ 
    public string name; 
} 
  
public class Person_ref 
{ 
    public string name; 
}

則運行結果爲:

實驗5的運行結果似乎說明即使參數是類(class),只要不加ref,也是傳值調用。

下面就引出了我的理解。

2. 沒有ref時,即使參數爲引用類型(class)時,也可算是一種傳值調用

參數爲引用類型時,傳遞的是該引用類型的地址的一份拷貝,“該引用類型的地址的一份拷貝”即爲傳值調用的“值”。

注意這裏說傳遞的是該引用類型的地址的一份拷貝,而不是引用類型的地址。

下面將用圖的形式來說明以上實驗3,實驗5和實驗6中內存的情況。

2.1 首先是實驗3

實驗3的內存圖如下,實參是函數ChangeByClass外的Person_ref對象,形參是函數ChangeByClass內的Person_ref對象。

捕獲 

從圖中我們可以看出實參new出來之後就在託管堆上分配了內存,並且在棧上保存了對象的指針。

調用函數ChangeByClass後,由於沒有ref參數,所以將棧上的實參p_val拷貝了一份作爲形參,注意這裏p_val(實參)p_val(形參)是指向託管堆上的同一地址。

所以說沒有ref時,即使參數爲引用類型(class)時,也可算是一種傳值調用,這裏的值就是託管堆中對象的地址(0x1000)。

調用函數ChangeByClass後,通過p_val(形參)修改了name屬性的值,由於p_val(實參)p_val(形參)是指向託管堆上的同一地址,所以函數外的p_val(實參)的name屬性也被修改了。

捕獲

2.2 然後是實驗5

上面的實驗3從執行結果來看似乎是傳引用調用,因爲形參的改變導致了實參的改變。

下面的實驗5就可以看出,p_val(形參)p_val(實參)並不是同一個變量,而是p_val(實參)的一個拷貝。

捕獲

從圖中可以看出第一步還是和實驗3一樣,但是在調用函數ChangeByClassNew後,就不一樣了。

函數ChangeByClassNew中,對p_val(形參)重新分配了內存(new操作),使其指向了新的地址(0x1100),如下圖:

捕獲

所以p_val(形參)的name屬性改了時候,p_val(實參)的name屬性還是沒變。

2.3 最後是實驗6

我覺得實驗6是真正的傳引用調用。不廢話了,直接上第一個圖。

捕獲

參數中加了ref關鍵字之後,其實傳遞的不是託管堆中對象的地址(0x1000),而是棧上p_val(實參)的地址(0x0001)。

所以這裏實參和形參都是棧上的同一個東西,沒有什麼區別了。我覺得這纔是真正的傳引用調用。

然後調用了函數ChangeByClassRefNew,函數中對p_val(形參)重新分配了內存(new操作),使其指向了新的地址(0x1100)。

捕獲

由於p_val(形參)就是p_val(實參),所以p_val(形參)的name屬性改變後,函數ChangeByClassRefNew外的p_val(實參)的name屬性也被改變了。

而原先分配的對象(地址0x1000)其實已經沒有被引用了,隨時會被GC回收。

3. 結論

如果傳遞的參數是基元類型(int,float等)或結構體(struct),那麼就是傳值調用。 如果傳遞的參數前有ref或者out關鍵字,那麼就是傳引用調用。 如果傳遞的參數是類(class)並且沒有ref或out關鍵字:
  1. 如果調用的函數中對參數重新進行了地址分配(new操作),那麼執行結果類似傳值調用
  2. 如果調用的函數中沒有對參數重新進行了地址分配,直接就是使用了傳遞的參數,那麼執行結果類似傳引用調用

原文鏈接:http://www.cnblogs.com/wang_yb/archive/2011/05/18/2050574.html

發佈了8 篇原創文章 · 獲贊 8 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章