緣起
其實這篇文章在 草稿 裡已有一段時間, 但因為有些細節沒有確切的答案, 所以一直沒有沒有發佈.
直到最近參加了 SkillTree 主辦, Bill叔 主講的 物件導向實作課程(使用C#)第四梯 , 對於細節部份, 得到了一些解答, 也有了比較大的信心, 故重新整理後, 進行發佈.
完整範例, 請由此下載.
C# 的參數傳遞, 主要有 2 種:
這 2 種方式到底有什麼不同呢? 經 Bill叔 的課程解惑, 主要在於:
以下區分為不同資料型別 (值型別, 不可變的參考型別, 可變的參考型別), 在 Call By Value 及 Call By Reference 下的狀況, 共有 6 個情境.
1. 值型別 (Value Type), 如: int, float ...
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容, 如上圖的 "綠" 字
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref int inId) 的異動, 會反映到父程序. 如上圖的 "深綠" 字
2. 不可變的參考型別 (Immutable Reference Type), 如: string
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容 (此內容是指向實際字串資料的位址), 如上圖的 "藍" 字; 既然如此, 父/子程序的字串變數, 都指向同一個資料位址, 那麼, 按理子程序修改內容, 應該要反映到主程序?
但這個在 C# String 的運作, 並非如此 ...子程序裡的 = 運算子, 其右方的字串, 會在記憶體佔有一塊區域, 其實是將參數 (ex: string inStr) 的內容, 改換為指向該區域的位址; 並非改原來指向位址的資料內容 ("ABC").
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref string inStr) 的異動, 會反映到父程序, 如上圖的 "深藍" 字; 故修改其內容, 回到主程序, 也會改掉.
3. 可變的參考型別 (Mutable Reference Type), 如: object
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容, (此內容是指向實際物件資料的位址), 如上圖的 "黃" 字; 既然如此, 父/子程序的物件變數, 都指向同一個資料位址, 那麼, 按理子程序修改內容, 應該要反映到主程序? 這個在 C# 的運作, 確實如此.
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref string inCust) 的異動, 會反映到父程序, 如上圖的 "深黃" 字; 故修改其內容, 回到主程序, 也會改掉.
C# String 雖然是 Reference Type, 但作參數傳遞時的實際行為, 卻與物件不同, 所以要特別留意.
(註: 謝謝 Bill叔的指點, 修正文章內容, 以免讓讓者產生誤解)
C# String 是 Reference Type, 所以其參數傳遞時的行為, 與物件是相同的, 只是字串屬於不可變的參考型別 (Immutable Reference Type), 每次指派一個新的字串值時 (ex: inStr="DEF" or inStr=strTemp), 會有2種狀況:
(1) 在等號右方為已知的字串值時, 編譯器會事先配置該字串值至 String Pool; 故字串變數的內容, 會成為指向該 String Pool 某個元素的位址.
(2) 在等號右方為未知的字串值時 (例如: 使用者輸入, 外部檔案讀入 ...), 編譯器無法事先優化, 故會在 Heap 配置一塊新的記憶體, 同時, 更改字串變數的內容為該新配置記憶體的位址.
關於重新指派字串變數時, 會發生的記憶體變化 (.NET 2.0 以後, 會透過 String Pool 進行優化), 有興趣者, 可以參考筆者的另外一篇文章: C# String: String.Empty is more efficient than "" ?
另外, 請注意: Reference Type 與 Call By Reference 是不同的東西; 前者是資料型別, 後者是參數傳遞方式, 不要混淆了.
直到最近參加了 SkillTree 主辦, Bill叔 主講的 物件導向實作課程(使用C#)第四梯 , 對於細節部份, 得到了一些解答, 也有了比較大的信心, 故重新整理後, 進行發佈.
完整範例, 請由此下載.
導火線
日前接獲一位朋友告知, 表示 C# String 是 Reference Type, 按理作為參數, 應該跟傳物件一樣, 在子程序修改字串內容, 應該反映到主程序才對, 但實測結果並非如此 ...問題呈現
以下是簡化後的程式碼:
其結果如下圖, 可以發現在沒有加 ref 的狀況下, 回到主程序, 不會改變原來主程序裡的字串內容.
綜合網路上的文章, 重新架構, 撰寫如下的測試程式, 包括了 值型別(ValueType), 參考型別(ReferenceType, 包括: string, 物件) 的參數傳遞.
其結果如下圖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class Program { private static void Change( string inStr) { inStr = "DEF" ; Console.WriteLine( "[Change] inStr={0}" , inStr); } private static void ChangeByRef( ref string inStr) { inStr = "DEF" ; Console.WriteLine( "[ChangeByRef] inStr={0}" , inStr); } static void Main( string [] args) { //======================================= //字串參數傳遞問題重現 測試: //======================================= string strX = "ABC" ; //呼叫 Change Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine( "[Main](before call Change()) strX={0}" , strX); Change(strX); Console.WriteLine( "[Main](after call Change()) strX={0}" , strX); //呼叫 ChangeByRef Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine( "[Main](before call ChangeByRef()) strX={0}" , strX); ChangeByRef( ref strX); Console.WriteLine( "[Main](after call ChangeByRef()) strX={0}" , strX); } } |
其結果如下圖, 可以發現在沒有加 ref 的狀況下, 回到主程序, 不會改變原來主程序裡的字串內容.
![]() |
原始問題重現 |
探索原因 (1)
確認有上述問題後, 在網路上查了幾篇文章, 其中 How are strings passed in .NET? 這篇文章對於字串參數的傳遞, 有詳細的說明,
探索原因 (2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | /// <summary> /// 用以測試傳遞物件的狀況 /// </summary> public class Customer { public int Id { get ; set ; } public string Name { get ; set ; } public string GetString() { return String.Concat( "Id: " , this .Id, " Name: " , this .Name); } } /// <summary> /// Console 主程式 /// </summary> class Program { #region 字串參數傳遞問題重現 private static void Change( string inStr) { inStr = "DEF" ; Console.WriteLine( "[Change] inStr={0}" , inStr); } private static void ChangeByRef( ref string inStr) { inStr = "DEF" ; Console.WriteLine( "[ChangeByRef] inStr={0}" , inStr); } #endregion #region ValueType 呼叫 private static void ValueTypeCall( int inId) //##1.2## { inId = 10; //##1.3## Console.WriteLine( "[ValueTypeCall] id={0}" , inId); } private static void ValueTypeCallByRef( ref int inId) //##2.2## { inId = 10; //##2.3## Console.WriteLine( "[ValueTypeCallByRef] id={0}" , inId); } #endregion #region StringType 呼叫 private static void StringTypeCall( string inStr) //##3.2## { inStr = "DEF" ; //##3.3## Console.WriteLine( "[StringTypeCall] inStr={0}" , inStr); } private static void StringTypeCallByRef( ref string inStr) //##4.2## { inStr = "DEF" ; //##4.3## Console.WriteLine( "[StringTypeCallByRef] inStr={0}" , inStr); } #endregion #region ObjectType 呼叫 private static void ObjectTypeCall(Customer inCust) //##5.2## { inCust.Name = "Jasper" ; //##5.3## Console.WriteLine( "[ObjectTypeCall] Id={0}, Name={1}" , inCust.Id, inCust.Name); } private static void ObjectTypeCall( ref Customer inCust) //##6.2## { inCust.Name = "Jasper" ; //##6.3## Console.WriteLine( "[ObjectTypeCall] Id={0}, Name={1}" , inCust.Id, inCust.Name); } #endregion static void Main( string [] args) { //======================================= //字串參數傳遞問題重現 測試: //======================================= string strX = "ABC" ; //呼叫 Change Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine( "[Main](before call Change()) strX={0}" , strX); Change(strX); Console.WriteLine( "[Main](after call Change()) strX={0}" , strX); //呼叫 ChangeByRef Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine( "[Main](before call ChangeByRef()) strX={0}" , strX); ChangeByRef( ref strX); Console.WriteLine( "[Main](after call ChangeByRef()) strX={0}" , strX); //======================================= //Value Type 測試: //======================================= int id = 1; //##1.1## //呼叫 ValueTypeCall Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine( "[Main](before call ValueTypeCall()) id={0}" , id); ValueTypeCall(id); Console.WriteLine( "[Main](after call ValueTypeCall()) id={0}" , id); //##1.4## //呼叫 ValueTypeCallByRef Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine( "[Main](before call ValueTypeCallByRef()) id={0}" , id); ValueTypeCallByRef( ref id); Console.WriteLine( "[Main](after call ValueTypeCallByRef()) id={0}" , id); //##2.4## //======================================= //Reference Type 測試: (String) //======================================= string str = "ABC" ; //##3.1## //呼叫 StringTypeCall Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine( "[Main](before call StringTypeCall()) str={0}" , str); StringTypeCall(str); Console.WriteLine( "[Main](after call StringTypeCall()) str={0}" , str); //##3.4## //呼叫 StringTypeCallByRef Console.ForegroundColor = ConsoleColor.DarkBlue; Console.WriteLine( "[Main](before call StringTypeCallByRef()) str={0}" , str); StringTypeCallByRef( ref str); Console.WriteLine( "[Main](after call StringTypeCallByRef()) str={0}" , str); //##4.4## //======================================= //Reference Type 測試: (Object) //======================================= //呼叫 ObjectTypeCall Console.ForegroundColor = ConsoleColor.Yellow; Customer cust = new Customer { Id = 1, Name = "Tester" }; //##5.1## Console.WriteLine( "[Main](before call ObjectTypeCall()) Id={0}, Name={1}" , cust.Id, cust.Name); ObjectTypeCall(cust); Console.WriteLine( "[Main](after call ObjectTypeCall()) Id={0}, Name={1}" , cust.Id, cust.Name); //##5.4## //呼叫 ObjectTypeCallByRef Console.ForegroundColor = ConsoleColor.DarkYellow; cust = new Customer { Id = 1, Name = "Tester" }; //##6.1## Console.WriteLine( "[Main](before call ObjectTypeCallByRef()) Id={0}, Name={1}" , cust.Id, cust.Name); ObjectTypeCall( ref cust); Console.WriteLine( "[Main](after call ObjectTypeCallByRef()) Id={0}, Name={1}" , cust.Id, cust.Name); //##6.4## Console.ReadLine(); } } |
其結果如下圖:
![]() |
範例程式執行結果 |
探索原因 (3)
- 在不加 ref 修飾字的狀況下, 是 Call By Value
- 在加 ref 修飾字的狀況下, 是 Call By Reference
這 2 種方式到底有什麼不同呢? 經 Bill叔 的課程解惑, 主要在於:
- Call By Value : 在傳遞參數時, 會多配置一份記憶體空間給形式參數 (formal parameter)( 就是子程序定義的參數), 以便將父程序的實際參數 (actual parameter) 的內容複製一份.
- Call By Reference : 在傳遞參數時, 不會多配置一份記憶體空間給形式參數 (formal parameter). 可以將之視為原來傳入的實際參數 (actual parameter) 的別名, 共用相同的記憶體空間, 這樣的想法, 會比較單純.
以下區分為不同資料型別 (值型別, 不可變的參考型別, 可變的參考型別), 在 Call By Value 及 Call By Reference 下的狀況, 共有 6 個情境.
1. 值型別 (Value Type), 如: int, float ...
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容, 如上圖的 "綠" 字
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref int inId) 的異動, 會反映到父程序. 如上圖的 "深綠" 字
2. 不可變的參考型別 (Immutable Reference Type), 如: string
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容 (此內容是指向實際字串資料的位址), 如上圖的 "藍" 字; 既然如此, 父/子程序的字串變數, 都指向同一個資料位址, 那麼, 按理子程序修改內容, 應該要反映到主程序?
但這個在 C# String 的運作, 並非如此 ...子程序裡的 = 運算子, 其右方的字串, 會在記憶體佔有一塊區域, 其實是將參數 (ex: string inStr) 的內容, 改換為指向該區域的位址; 並非改原來指向位址的資料內容 ("ABC").
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref string inStr) 的異動, 會反映到父程序, 如上圖的 "深藍" 字; 故修改其內容, 回到主程序, 也會改掉.
3. 可變的參考型別 (Mutable Reference Type), 如: object
(1) Call By Value: 會在子程序的參數, 配置一個新的記憶體位址, 以存放的是傳入參數的內容, (此內容是指向實際物件資料的位址), 如上圖的 "黃" 字; 既然如此, 父/子程序的物件變數, 都指向同一個資料位址, 那麼, 按理子程序修改內容, 應該要反映到主程序? 這個在 C# 的運作, 確實如此.
(2) Call By Reference: 可以將子程序的參數, 視為是傳入參數的別名; 在子程序對該參數變數 (ex: ref string inCust) 的異動, 會反映到父程序, 如上圖的 "深黃" 字; 故修改其內容, 回到主程序, 也會改掉.
總結
如果真的還是不清楚, 最好畫張圖, 這樣會比較容易了解.
(註: 謝謝 Bill叔的指點, 修正文章內容, 以免讓讓者產生誤解)
C# String 是 Reference Type, 所以其參數傳遞時的行為, 與物件是相同的, 只是字串屬於不可變的參考型別 (Immutable Reference Type), 每次指派一個新的字串值時 (ex: inStr="DEF" or inStr=strTemp), 會有2種狀況:
(1) 在等號右方為已知的字串值時, 編譯器會事先配置該字串值至 String Pool; 故字串變數的內容, 會成為指向該 String Pool 某個元素的位址.
(2) 在等號右方為未知的字串值時 (例如: 使用者輸入, 外部檔案讀入 ...), 編譯器無法事先優化, 故會在 Heap 配置一塊新的記憶體, 同時, 更改字串變數的內容為該新配置記憶體的位址.
關於重新指派字串變數時, 會發生的記憶體變化 (.NET 2.0 以後, 會透過 String Pool 進行優化), 有興趣者, 可以參考筆者的另外一篇文章: C# String: String.Empty is more efficient than "" ?
另外, 請注意: Reference Type 與 Call By Reference 是不同的東西; 前者是資料型別, 後者是參數傳遞方式, 不要混淆了.
謝謝您的分享,講解得很清楚
回覆刪除不客氣 ^^
刪除