ref 與 out 同樣能讓方法修改呼叫端變數,但設計意圖、編譯器限制、IL 行為截然不同。本文從適用場景切入,層層深入至值類型/引用類型語義差異、IL 指令對比、常見陷阱,助你徹底掌握兩者精髓。
ref:傳遞變數引用 (如 C/C++ 指標),雙向資料流,呼叫端必須預先初始化。
out:實現多返回值,單向資料流 (方法→呼叫端),呼叫端無需初始化,方法必須賦值。
| 方式 | 語法 | 呼叫端初始化 | 方法內賦值 | 資料流向 | 適用場景 |
|---|---|---|---|---|---|
| 傳值 | void M(int x) | 必須 | 修改副本 | 單向入 | 唯讀輸入 |
| ref | void M(ref int x) | 必須 | 可讀可寫 | 雙向 | 需修改原值、避免大結構體複製 |
| out | void M(out int x) | 不需 | 必須寫入 | 單向出 | 額外返回值、TryParse 模式 |
int a;
TestOut(out a); // ✓ 允許未初始化
TestRef(ref a); // ✗ CS0038: 使用未賦值的區域變數
// 類比:out ≈ return
// return: 呼叫端無需初始化變數,方法必須 return
// out: 呼叫端無需初始化變數,方法必須賦值
// C#
void TestOut(out int a) { a = 1; }
void TestRef(ref int a) { a = 1; }
// IL (簡化)
.method void TestOut(int32& a) { // 參數標記 [out]
ldarg.0
ldc.i4.1
stind.i4
ret
}
.method void TestRef(int32& a) { // 無 [out] 標記
ldarg.0
ldc.i4.1
stind.i4
ret
}
結論:IL 皆為 int32& (傳址),唯一差異是參數上的 [out] 自訂屬性,供編譯器/驗證器檢查規則。
object a = new object(), b = new object(), c = new object();
TestOut(out a); // a = null
TestRef(ref b); // b = null
TestVal(c); // c 仍指向原物件
void TestOut(out object o) { o = null; }
void TestRef(ref object o) { o = null; }
void TestVal(object o) { o = null; }
傳值 (TestVal):
棧框架 堆 +------+ +----------+ | o | ───→ | Object | ← c 也指向這裡 +------+ +----------+ o = null 只切斷區域變數 o 的引用,c 不受影響
ref (TestRef):
呼叫端棧 方法棧框架 堆 +------+ +------+ +----------+ | b | ◄── | o | ──→ | Object | +------+ +------+ +----------+ o = null 修改的是呼叫端的 b (傳的是 b 的位址),b 變 null
// TestVal(object o)
ldnull
starg.s o // 直接存入參數槽
// TestRef(ref object o)
ldarg.0 // 關鍵:先載入參數位址 (指向呼叫端變數)
ldnull
stind.ref // 透過位址間接存入
ldarg.0 使方法拿到呼叫端變數的位址 (指標的指標),而非物件位址。
int, struct)list = new List())sizeof > 16 bytes) 避免複製開銷 (readonly ref C# 7.2+)bool TryGetValue(key, out value)void Initialize(out Config cfg)void Deconstruct(out int x, out int y)list.Add(), obj.Prop = val) → 直接傳值即可in (C# 7.2+)| 陷阱 | 後果 | 對策 |
|---|---|---|
| 引用類型用 ref 卻只改屬性 | 多此一舉,誤導維護者 | 改傳值 |
| 引用類型傳值卻重新 new | 呼叫端變數仍指向舊物件 | 改 ref 或回傳新物件 |
| out 參數方法所有路徑未賦值 | 編譯錯誤 CS0177 | 確保所有分支賦值 |
| ref 參數傳遞屬性/索引器 | 編譯錯誤 (非變數) | 用區域變數暫存 |
in (C# 7.2):readonly ref,唯讀傳址,避免大結構體複製ref readonly 返回值:避免複製且防修改ref struct (C# 7.2):Span<T> 等棧限型別,只能用 ref/out/inout var (C# 7.0):TryParse(s, out var i) 內聯宣告ref = 「給我這個變數的位址,我可能讀也可能改」
out = 「給我這個變數的位址,我保證會寫入新值 (作為額外返回值)」
引用類型 + ref = 雙重間址,能替換呼叫端變數綁定的物件;引用類型傳值 = 單重間址,只能修改物件內容。
依據資料流向 (入/出/雙向) 與類型語義 (值/引用) 選擇,而非死記規則,才能寫出意圖清晰、無 Bug 的代碼。