許多開發者知道 ref 和 out 代表「傳參考」,但對其與一般傳值(ByVal)在方法簽名、IL 指令、編譯期行為上的細微差異往往模糊不清。本文透過 IL 反組譯碼,從執行層面徹底釐清三者差異。
static void Main()
{
Int32 p;
TestRef(out p); // ① out 參數
// TestRef(ref p); // ② ref 參數
TestRef(p); // ③ 傳值
}
static void TestRef(Int32 para) { para = 1; } // ④ 傳值
static void TestRef(out Int32 para) { para = 2; } // ⑤ out
// static void TestRef(ref Int32 para) { para = 3; } // ⑥ ref
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init ([0] int32 p)
L_0000: ldloca.s p // 取區域變數 p 的位址 → 压栈
L_0002: call void TestRef(int32&) // 呼叫 out/ref 版本
L_0007: ldloc.0 // 取區域變數 p 的值 → 壓栈
L_0008: call void TestRef(int32) // 呼叫傳值版本
L_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
L_0012: pop
L_0013: ret
}
關鍵差異:
ldloca.s (Load Local Address) → 傳遞位址(int32&)
ldloc.0 (Load Local Value) → 傳遞值副本(int32)
| 方法 | IL 簽名 | 關鍵標記 |
|---|---|---|
| TestRef(Int32) | .method ... TestRef(int32 para) | 純值型別參數 |
| TestRef(ref Int32) | .method ... TestRef(int32& para) | & 表傳址 |
| TestRef(out Int32) | .method ... TestRef([out] int32& para) | [out] + & |
ref 與 out 在 IL 層面皆為 int32&(傳址),唯一差別在 out 參數多一個 [out] 自訂屬性標記。
// ref 版本 / out 版本(主體完全相同)
.method ... TestRef(int32& para)
{
L_0000: ldarg.0 // 載入參數位址
L_0001: ldc.i4.2 // 載入常數 2
L_0002: stind.i4 // 將值存入位址指向的記憶體
L_0003: ret
}
// 傳值版本
.method ... TestRef(int32 para)
{
L_0000: ldc.i4.1 // 載入常數 1
L_0001: starg.s para // 存入參數槽(僅影響區域副本)
L_0003: ret
}
stind.i4 (Store Indirect) 透過位址寫入記憶體,影響原始變數;
starg.s (Store Argument) 僅修改參數區域副本。
ldloca.s 取址 vs ldloc 取值,對應「傳參考」與「傳值」。int32&,僅 [out] 標記不同,CLR 視為相同簽名,編譯器禁止同時存在。ref 需預先初始化;out 不需初始化,但方法內必須指派值。TryParse 模式),語意上屬「單向輸出參數」。ref(雙向)。out(語意更清晰)。ref/out,優先考慮回傳值、Tuple、自訂結果型別,保持方法純粹性。