ref:「我想在函數內**修改**呼叫端的變量(讀寫)」—— 傳遞**變量引用**(棧指標)。
out:「我想從函數**輸出**一個額外結果(單向寫入)」—— 語義上等價於**多返回值**,編譯器強制要求函數內賦值。
理解這一句,**選型不再猶豫**。
&)。&)。.method private hidebysig static void Test1([out] int32& a) cil managed
{
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.1
IL_0003: stind.i4
IL_0004: ret
}
.method private hidebysig static void Test2(int32& a) cil managed // ref
{
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldc.i4.1
IL_0003: stind.i4
IL_0004: ret
}
// 函數體 IL 完全相同!區別僅在簽名標記 [out] 與調用端檢查規則。
引用類型變量本身存儲**對象引用(堆地址)**。
obj.Field = ...)→ 外部可見。obj = new Obj() 或 obj = null)→ **外部不可見**!因為只是改了副本指向。obj = new Obj() → **外部變量指向新對象**!Object**)。ref 傳遞機制(棧引用)。null 或新實例)。// 直接傳遞
Main: obj1 ──────→ [Heap Object A]
↓ 值傳遞(複製引用)
Func: param ─────→ [Heap Object A] // 同一對象
param = new Obj() // param → [Heap Object B], obj1 仍指向 A
// ref 傳遞
Main: obj1 ──────→ [Heap Object A]
↓ 傳遞 &obj1 (棧地址)
Func: param (指向 obj1 的指標)
*param = new Obj() // obj1 現在指向 [Heap Object B]!
out(或 Tuple/ValueTuple C# 7+)。ref。ref。where T : struct 且需修改? → ref T。ref/out**,改用 Task<Tuple> 或 out 參數包裝類。| 陷阱 | 後果 | 規避 |
|---|---|---|
引用類型直接傳遞時 param = null | 外部變量不受影響,誤以為已清空 | 改用 ref 或返回新值 |
| ref 參數未初始化就傳入 | 編譯錯誤 CS0165 | 調用前 int x = 0; |
| out 參數函數內未賦值 | 編譯錯誤 CS0177 | 所有分支 outParam = ... |
| 遞歸/迭代器中使用 ref/out | 編譯錯誤 | 不可在 yield return / await 上下文使用 |
| 介面/委託簽名不匹配 | 實現失敗 | 介面/委託需同樣標記 ref/out |
(bool success, int value, string msg) TryParse(...) —— 多返回值首選,無 out 攝雜。ref 返回。ref = **雙向通道**(讀寫),out = **單向輸出管道**(寫入)。兩者在 IL 層面同為 &(托管指標),差異僅在**編譯器施加的語義約束**。掌握「棧引用 vs 堆引用」記憶體模型,引用類型的 ref 行為即豁然開朗。新代碼優先考慮 ValueTuple、in、readonly ref,讓意圖顯性化,減少副作用。