1. 前言
在 C# 裡,預設所有方法參數都是值傳遞,也就是說傳進去的是複製品,不是原本的變數。
可是如果你想要在方法裡面改外面的變數怎麼辦?或者想要一次從方法回傳好幾個值?又或者你要傳一個很大的 struct 進去,但又不想複製整個「沒有把手的行李箱」,只想省記憶體傳個參考就好?
這些情境都跟參數修飾詞有關。它們讓你可以掌控資料在呼叫端跟方法之間的「傳遞路線」。
修飾詞種類
| 修飾詞 | 資料傳遞 | 呼叫前要初始化嗎? | 裡面可以讀嗎? | 裡面可以寫嗎? | 常見使用場景 |
|---|---|---|---|---|---|
| ref | 雙向 | 要 | 可以 | 可以 | 傳變數進去讀或改 |
| out | 只往外 | 不用 | 不行(賦值前) | 一定要(強制) | 從方法回傳多個值 |
| in | 只往內 | 要 | 可以 | 不行 | 傳大 struct 進去「唯讀」又省記憶體 |
別擔心,等等我們會一個一個看,其實比你想像的簡單多了。
2. ref 修飾詞:傳參考,能讀也能寫
想像一下:你拿一杯咖啡給朋友,跟他說:「這杯你可以喝、可以加熱、甚至想加什麼都行。」朋友可以對這杯咖啡為所欲為,最後剩下什麼(或沒剩什麼)都還你。這就是 ref 的概念。
void DoSomething(ref int x)
{
x = x + 10; // 改變傳進來的參數
}
要用 ref 傳參數,宣告跟呼叫都要加這個修飾詞:
int myNumber = 5;
DoSomething(ref myNumber);
Console.WriteLine(myNumber); // 會印出 15
一定要是變數,不能傳運算式。這樣會出錯:
DoSomething(ref 10); // 這裡一定要是變數!
- 呼叫前:變數一定要初始化(要有值)。
- 方法內:可以讀也可以改。
- 呼叫後:改動會保留。
範例:交換兩個變數的值
有時候要交換兩個變數的值。但有個重點:C# 的值型別(像 int)預設是值傳遞。也就是說傳進方法的是值的複製品,不是原本的變數。要讓方法能改呼叫端的變數,就要用 ref。
沒用 ref 的範例 — 不會動:
// 嘗試交換值 — 不會成功
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(x, y); // 傳的是值的複製品
Console.WriteLine($"{x}, {y}"); // → 10, 20 — 完全沒變!
在 Swap 函數裡 a 跟 b 只是 x 跟 y 的複製品。我們只改到複製品,原本的沒動到。
用 ref 的範例 — 真的交換了
// 用 ref 交換變數的值
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(ref x, ref y); // 傳的是變數的參考
Console.WriteLine($"{x}, {y}"); // → 20, 10 — 真的換了
ref 讓你傳的是變數的參考,不是值。
所以 Swap 直接操作 x 跟 y,內容就被換掉了。
什麼時候該用 ref?
- 你想直接改傳進來的變數(像加一、換掉)。
- 不想用 return 回傳新值,而是直接改參數。
- 不要用來「塞一堆值」出去 — 這種情境用別的修飾詞比較好。
3. out 修飾詞:只往外傳
換個想像:你帶著空杯子去找朋友,跟他說:「幫我裝點東西,咖啡茶都行!」這時你朋友一定要幫你裝點什麼,不然編譯器會發飆。
out 就是讓方法能回傳多個值,不用靠 return。這種參數一定要在方法裡被賦值。
void GetCoordinates(out int x, out int y)
{
x = 5;
y = 10; // 沒這行會錯!
}
- 呼叫前:變數可以不用初始化,甚至可以直接在呼叫時寫 out int x。
- 方法內:一定要賦值,不然編譯器會罷工。
- 呼叫後:變數有新值。
範例:一次回傳兩個值
void ParseNameAndAge(string input, out string name, out int age)
{
string[] parts = input.Split(','); // 把字串切成陣列 - 以 ',' 分割
name = parts[0];
age = int.Parse(parts[1]);
}
string userInput = "Иван,25";
ParseNameAndAge(userInput, out string userName, out int userAge);
Console.WriteLine($"{userName} — {userAge} 歲");
什麼時候該用 out?
- 你想從方法回傳多個值(像上面把字串拆開)。
- 回傳型別不一定(像嘗試把字串轉成數字)。
.NET 經典範例: int.TryParse(string, out int) — 超常用!
string input = "123";
if (int.TryParse(input, out int parsedNumber))
{
Console.WriteLine($"轉換成功: {parsedNumber}");
}
else
{
Console.WriteLine("轉換失敗!");
}
int.TryParse 會回傳 true 代表轉換成功,false 代表失敗。數字本身是透過 out 回傳的。
用 ref 跟 out 常見錯誤跟趣事
- 忘記初始化 ref 變數 — 編譯器會生氣。
- 沒給 out 變數賦值 — 編譯器更生氣。
- 呼叫時忘了加修飾詞:寫 DoSomething(x) 而不是 DoSomething(ref x)。
- ref 跟 out 不能用常數或字面值!只能用變數。只有變數才有記憶體位址可以傳參考。
4. in 修飾詞:唯讀參考
有時候你要把一個很大很複雜的物件傳進方法。通常這種物件會用參考傳,但這樣方法就能隨便改它 — 因為你給了它參考。
理論上你可以複製一份再傳進去,這樣原本的不會被改。但如果物件很大,這樣會白白複製一堆資料。最省的方法是直接傳參考,然後請編譯器幫你確保沒人會改它。
in — 新的修飾詞,讓你可以把struct 用參考傳進去,但只能讀不能改。就像「玻璃櫃裡的寶劍」:只能看,不能拿。
void PrintPoint(in Point pt)
{
pt.X = 5; // 錯誤! 只能讀
Console.WriteLine($"點: {pt.X}, {pt.Y}"); //這樣可以
}
- 呼叫前:變數要初始化。
- 方法內:只能讀,不能改。
- 省記憶體:struct 不會被複製,是傳參考。
很適合用在大 struct 陣列,避免多餘的複製。不過對 class(參考型別)來說 in 沒什麼特別效果。
範例:傳大 struct 用參考,省複製
struct BigData
{
public int A, B, C, D, E;
}
void PrintBigData(in BigData data)
{
data.A = 10; // 不行 - 只能讀!
Console.WriteLine(data.A + data.B + data.C + data.D + data.E);
}
BigData myData = new BigData { A = 10, B = 20, C = 30, D = 40, E = 50 };
PrintBigData(in myData);
5. 常見問題:
ref/out/in 可以用在陣列跟參考型別嗎?
可以!但要記得:陣列本身就是參考型別。用 ref 傳陣列時,你可以改變參考本身。通常這種修飾詞比較常用在 struct。
ref 跟 out 有什麼差別?不是都能拿到值?
ref 變數一定要先初始化才能用。out 不用,但在方法裡一定要賦值一次。
如果我用 ref/out/in 傳字面值(像 5)會怎樣?
編譯器馬上報錯。只能用變數!
ref/out/in 可以用幾個參數?
幾個都行,沒上限!但為了程式可讀性,建議不要超過兩三個,不然可以考慮用 tuple、struct 或 class。
GO TO FULL VERSION