傳值調用和傳引用調用是幾乎所有主流語言都會涉及到的問題,下面我談談我對C#中傳值調用和傳引用調用的理解。
驗證示例的代碼如下:
view sourceprint?
01 using System;
02
03 public class ArgsByRefOrValue
04 {
05 public static void Main(string[] args)
06 {
07 // 實驗1. 傳值調用--基元類型
08 int i = 10;
09 Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
10 ChangeByInt(i);
11 Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
12
13 Console.WriteLine("==============================================");
14 // 實驗2. 傳值調用--結構體
15 Person_val p_val = new Person_val();
16 p_val.name = "old val name";
17 Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
18 ChangeByStruct(p_val);
19 Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
20
21 Console.WriteLine("==============================================");
22 // 實驗3. 傳引用調用--類
23 Person_ref p_ref = new Person_ref();
24 p_ref.name = "old ref name";
25 Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
26 ChangeByClass(p_ref);
27 Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
28
29 Console.WriteLine("==============================================");
30 // 實驗4. 傳引用調用--利用ref
31 Person_ref p = new Person_ref();
32 p.name = "old ref name";
33 Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
34 ChangeByClassRef(ref p);
35 Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
36
37 Console.ReadKey(true);
38 }
39
40 static void ChangeByInt(int i)
41 {
42 i = i + 10;
43 Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
44 }
45
46 static void ChangeByStruct(Person_val p_val)
47 {
48 p_val.name = "new val name";
49 Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
50 }
51
52 static void ChangeByClass(Person_ref p_ref)
53 {
54 p_ref.name = "new ref name";
55 Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
56 }
57
58 static void ChangeByClassRef(ref Person_ref p)
59 {
60 p.name = "new ref name";
61 Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
62 }
63 }
64
65 public struct Person_val
66 {
67 public string name;
68 }
69
70 public class Person_ref
71 {
72 public string name;
73 }
運行結果如下:
看起來似乎上面代碼中實驗3和實驗4是一樣的,即對於類(class)來說,不管加不加ref或out,都是傳引用調用。
其實,這只是表面的現象,只要稍微改一下代碼,結果就不一樣了。
修改上面代碼,再增加兩個實驗。
001 using System;
002
003 public class ArgsByRefOrValue
004 {
005 public static void Main(string[] args)
006 {
007 // 實驗1. 傳值調用--基元類型
008 int i = 10;
009 Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
010 ChangeByInt(i);
011 Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
012
013 Console.WriteLine("==============================================");
014 // 實驗2. 傳值調用--結構體
015 Person_val p_val = new Person_val();
016 p_val.name = "old val name";
017 Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
018 ChangeByStruct(p_val);
019 Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
020
021 Console.WriteLine("==============================================");
022 // 實驗3. 傳引用調用--類
023 Person_ref p_ref = new Person_ref();
024 p_ref.name = "old ref name";
025 Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
026 ChangeByClass(p_ref);
027 Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
028
029 Console.WriteLine("==============================================");
030 // 實驗4. 傳引用調用--利用ref
031 Person_ref p = new Person_ref();
032 p.name = "old ref name";
033 Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
034 ChangeByClassRef(ref p);
035 Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
036
037 Console.WriteLine("==============================================");
038 // 實驗5. 傳引用調用--類 在調用的函數重新new一個對象
039 Person_ref p_ref_new = new Person_ref();
040 p_ref_new.name = "old new ref name";
041 Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
042 ChangeByClassNew(p_ref_new);
043 Console.WriteLine("after call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
044
045 Console.WriteLine("==============================================");
046 // 實驗6. 傳引用調用--利用ref 在調用的函數重新new一個對象
047 Person_ref p_new = new Person_ref();
048 p_new.name = "old new ref name";
049 Console.WriteLine("before call ChangeByClassRefNew: p_new.name = " + p_new.name);
050 ChangeByClassRefNew(ref p_new);
051 Console.WriteLine("after call ChangeByClassRefNew: p_new.name = " + p_new.name);
052
053 Console.ReadKey(true);
054 }
055
056 static void ChangeByInt(int i)
057 {
058 i = i + 10;
059 Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
060 }
061
062 static void ChangeByStruct(Person_val p_val)
063 {
064 p_val.name = "new val name";
065 Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
066 }
067
068 static void ChangeByClass(Person_ref p_ref)
069 {
070 p_ref.name = "new ref name";
071 Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
072 }
073
074 static void ChangeByClassRef(ref Person_ref p)
075 {
076 p.name = "new ref name";
077 Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
078 }
079
080 static void ChangeByClassNew(Person_ref p_ref_new)
081 {
082 p_ref_new = new Person_ref();
083 p_ref_new.name = "new ref name";
084 Console.WriteLine("when calling ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
085 }
086
087 static void ChangeByClassRefNew(ref Person_ref p_new)
088 {
089 p_new = new Person_ref();
090 p_new.name = "new ref name";
091 Console.WriteLine("when calling ChangeByClassRefNew: p_new.name = " + p_new.name);
092 }
093 }
094
095 public struct Person_val
096 {
097 public string name;
098 }
099
100 public class Person_ref
101 {
102 public string name;
103 }
則運行結果為:
實驗5的運行結果似乎說明即使參數是類(class),只要不加ref,也是傳值調用。
下面就引出了我的理解。
2. 沒有ref時,即使參數為引用類型(class)時,也可算是一種傳值調用
參數為引用類型時,傳遞的是該引用類型的地址的一份拷貝,“該引用類型的地址的一份拷貝”即為傳值調用的“值”。
注意這裡說傳遞的是該引用類型的地址的一份拷貝,而不是引用類型的地址。
下面將用圖的形式來說明以上實驗3,實驗5和實驗6中內存的情況。
實驗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屬性也被修改了。
上面的實驗3從執行結果來看似乎是傳引用調用,因為形參的改變導致了實參的改變。
下面的實驗5就可以看出,p_val(形參)和p_val(實參)並不是同一個變量,而是p_val(實參)的一個拷貝。
從圖中可以看出第一步還是和實驗3一樣,但是在調用函數ChangeByClassNew後,就不一樣了。
函數ChangeByClassNew中,對p_val(形參)重新分配了內存(new操作),使其指向了新的地址(0x1100),如下圖:
所以p_val(形參)的name屬性改了時候,p_val(實參)的name屬性還是沒變。
我覺得實驗6是真正的傳引用調用。不廢話了,直接上第一個圖。
參數中加了ref關鍵字之後,其實傳遞的不是托管堆中對象的地址(0x1000),而是棧上p_val(實參)的地址(0x0001)。
所以這裡實參和形參都是棧上的同一個東西,沒有什麼區別了。我覺得這才是真正的傳引用調用。
然後調用了函數ChangeByClassRefNew,函數中對p_val(形參)重新分配了內存(new操作),使其指向了新的地址(0x1100)。
由於p_val(形參)就是p_val(實參),所以p_val(形參)的name屬性改變後,函數ChangeByClassRefNew外的p_val(實參)的name屬性也被改變了。
而原先分配的對象(地址0x1000)其實已經沒有被引用了,隨時會被GC回收。
作者:wang_yb