ref 與 out 都讓方法能修改呼叫端變數的值 (傳址語義),但**初始化要求、編譯器強制規則、適用場景**有本質差異。本文透過 IL 層面解析、編譯器規則、最佳實務全方位比較。
| 特性 | ref | out |
|---|---|---|
| 呼叫端變數初始化 | 必須 在呼叫前明確指派 | 不需要 (但必須已宣告) |
| 方法內部讀取參數 | 允許 (可讀取初始值) | 禁止 (CS0269 編譯錯誤) 必須先指派 |
| 方法內部指派要求 | 可選 (可不修改) | 強制 在所有路徑返回前必須指派 (CS0177) |
| IL 指令 | ldarg.1 / stind.ref (Managed pointer) | 同 ref,CLR 層面無差異,差異在編譯器驗證 |
| 多載解析 | 視為不同簽名,可與 out 共存 | 同左 |
| 遞迴/重入安全性 | 較低 (可讀取未初始化記憶體? 不,已初始化) | 較高 (編譯器保證寫入後才可讀) |
public void Increment(ref int value)
{
Console.WriteLine($"Before: {value}"); // 允許讀取
value++;
Console.WriteLine($"After: {value}");
}
// 呼叫端
int num = 10; // 必須初始化
Increment(ref num);
Console.WriteLine(num); // 11
public bool TryParseInt(string input, out int result)
{
// result = 0; // 若解析失敗仍需指派預設值
return int.TryParse(input, out result); // 直接傳遞 out
}
// 呼叫端
if (TryParseInt("123", out int parsed))
Console.WriteLine(parsed); // 123
// parsed 在此作用域可用 (C# 7+)
public void SplitPath(string fullPath, out string directory, out string fileName)
{
int idx = fullPath.LastIndexOfAny(new[] { '\\', '/' });
if (idx >= 0)
{
directory = fullPath.Substring(0, idx + 1);
fileName = fullPath.Substring(idx + 1);
}
else
{
directory = "";
fileName = fullPath;
}
}
// 呼叫
SplitPath(@"C:\Data\File.txt", out string dir, out string name);
// dir = "C:\Data\", name = "File.txt"
public void ReadOnlyLargeStruct(in BigStruct data)
{
// data 為 readonly,任何寫入嘗試編譯錯誤
// 避免大型結構體複製開銷,又保證不修改
Console.WriteLine(data.SomeField);
}
in = ref readonly:傳址但禁止修改,適用於**大型 readonly struct** 避免防禦性複製。readonly struct 使用效果最佳。ref/out/in 皆為 Managed Pointer (&),零複製、零配置。T&),允許修改呼叫端的參考指向 (如 ref List<T> 可指向新 List)。ref class 只在需「換掉整個物件」時使用。refoutin (C# 7.2+)out int x; Method(out x); → x 未宣告 (需 int x; 或 out int x 內嵌宣告 C# 7+)ref int x; Method(ref x); → x 未初始化 (需 int x = 0;)out 方法中讀取參數未先指派 → CS0269out 方法有路徑未指派參數 → CS0177