承接[上集](第十一回),本文聚焦引用類型參數傳遞的微妙差異、ref/out 本質比較、以及字串類型的特殊行為成因。
class ArgsByRef { public int i = 10; }
static void AddRef(ArgsByRef abf) { abf.i = 20; }
static void Main() {
var abf = new ArgsByRef();
AddRef(abf);
Console.WriteLine(abf.i); // 20 - 原物件被修改
}
本質:參數 abf 複製的是參考位址(指標值),雙方指向同一堆上物件,成員修改可見;但若在方法內 abf = new ArgsByRef(),外部變數不受影響(指向改變)。
ref/out):傳的是「引用的位址」| 傳遞方式 | 傳遞內容 | 效果 |
|---|---|---|
| 值類型按值 | 實例副本 | 不影響原值 |
| 引用類型按值 | 引用副本 (指標值) | 可改物件內容,不可換物件 |
值類型 ref | 變數位址 | 可改原值 |
引用類型 ref | 引用變數位址 (指標的指標) | 可換物件、可改內容 |
static void ChangeByRef(ref string s) { s = "New"; }
static void Main() {
string str = "Old";
ChangeByRef(ref str);
Console.WriteLine(str); // New - 原變數指向新物件
}
static void ChangeStr(string s) { s = "Changed"; }
static void Main() {
string str = "Original";
ChangeStr(str);
Console.WriteLine(str); // Original - 未改變
}
原因:string 是不可變 (immutable) 參考類型。s = "Changed" 在堆上建立新字串,s 指向新位址,外部 str 仍指向原字串。這是按值傳遞的正常行為,非特殊規則。若需修改外部變數,必須加 ref。
ref vs out:編譯期規則不同,IL 完全相同ref/out 不能構成多載(void M(ref int) vs void M(out int) 衝突)。ref:呼叫前必須初始化(確定記憶體位置)。out:呼叫前不可初始化,方法內必須賦值(確定輸出)。int x = 10; M(ref x); // OK
int y; M(out y); // OK,但 M 內必須 y = ...
// M(ref y); // 錯誤:未初始化
// int z = 10; M(out z); // 錯誤:out 參數不應預先賦值
params:編譯器語法糖void Sum(params int[] nums) { ... }
Sum(1, 2, 3); // 編譯器轉為 Sum(new int[]{1,2,3})
params 實質為 [ParamArrayAttribute],限制:必須為一維陣列、最後一個參數、僅能出現一次。
參數傳遞的核心在於「傳什麼」:值類型傳實例/位址,引用類型傳引用/引用位址。ref/out 統一為「傳址」,差異僅在編譯期驗證規則。理解記憶體模型(堆疊 vs 堆)是破解所有傳遞迷思的鑰匙。