1. 前言
在現實生活中,很多動作就像瑞士刀一樣多功能:同一個指令可以搭配不同的工具。例如,想像一下提款機:你插卡時,提款機會問你密碼;你輸入手機號碼時,提款機會等你輸入 SMS 驗證碼。動作都是「驗證使用者」,但方式不一樣。
寫程式時也常遇到這種情況:看起來是同一個操作,但資料型態或參數數量可能不同。比如,我們的方法要對人或動物打招呼,或是要加總兩個、三個,甚至十個整數。
當然,你可以把方法名字都取不一樣:SumTwo、SumThree、SumArray。但工程師都很懶(不是說懶是進步的動力嗎),而且這樣寫程式會變得很難讀。
方法多載
方法多載 就是讓同一個方法名稱可以處理不同參數組合的方法。這是多型的一種,但跟繼承沒關係。
方法多載就是可以在同一個 class(或 struct)裡面建立多個同名方法,但參數列表(型態、數量、順序)不一樣。
方法簽章
C# 裡方法簽章就是方法名稱加上參數型態和順序。回傳型態不算在簽章裡!這常常會讓人踩雷(等等會講)。
2. 多載實戰:簡單範例
來寫個 Greeter class,根據不同情況跟使用者打招呼:只用名字、名字加年齡,或是完全沒參數。
public class Greeter
{
// 沒參數的打招呼
public void Greet()
{
Console.WriteLine("哈囉,世界!");
}
// 用名字打招呼
public void Greet(string name)
{
Console.WriteLine($"哈囉,{name}!");
}
// 用名字和年齡打招呼
public void Greet(string name, int age)
{
Console.WriteLine($"哈囉,{name}!你已經 {age} 歲了?不錯喔!");
}
}
現在你可以呼叫任何一個方法,C# 編譯器會自動根據你給的參數數量和型態選對的版本。
var greeter = new Greeter();
greeter.Greet(); // 哈囉,世界!
greeter.Greet("安雅"); // 哈囉,安雅!
greeter.Greet("彼得", 23); // 哈囉,彼得!你已經 23 歲了?不錯喔!
3. 依參數型態和數量區分
多載會成立,只要方法有下列不同:
- 參數數量不同,
- 至少有一個參數型態不同,
- 參數型態順序不同(但這要小心)。
來加一個只接收年齡的多載版本:
public void Greet(int age)
{
Console.WriteLine($"這麼多年紀很酷!({age} 歲)");
}
現在呼叫:
greeter.Greet(10); // 這麼多年紀很酷!(10 歲)
要記得:如果方法只差回傳型態,是不能多載的。例如這樣會出錯:
// 編譯錯誤!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
編譯器會抱怨:「已經有 Foo(string) 了,來點有挑戰性的吧!」
4. 多載與 C# 標準函式庫
多載不只是我們自己寫的 Greet。打開 .NET Console.WriteLine 文件:
| 簽章 | 用途 |
|---|---|
|
印出空字串 |
|
印出字串 |
|
印出整數 |
|
印出小數 |
|
格式化字串(帶一個參數) |
|
格式化字串(多個參數) |
這些都是同一個方法 WriteLine 的多載。這就是為什麼你可以這樣寫:
Console.WriteLine("純字串");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("總和: {0}", 42);
編譯器都能正確判斷你要哪個版本!
5. 編譯器怎麼選多載?
很嚴格:它會看你實際傳進去的參數型態和數量。來個小表格:
| 呼叫 | 會選哪個版本? |
|---|---|
|
|
|
|
遇到模糊怎麼辦?
有時候會失控。多載太像時,編譯器選不出來:
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// 錯誤:模糊不清——到底要叫哪個 Print?(兩個都好像可以)
編譯器會報模糊錯誤。這種情況最好避免參數數量一樣、型態又很像的多載,不然會讓編譯器抓狂。
6. params —— 變數參數數量
假設你想寫個方法,可以接收不定數量的數字。這時就要用 params 關鍵字。
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"總和: {sum}");
}
你可以這樣呼叫:
SumAll(1, 2, 3); // 總和: 6
SumAll(10, 20); // 總和: 30
SumAll(); // 總和: 0
用 params 的方法可以跟多載一起用,但要小心不要讓編譯器搞不清楚你想叫哪個版本。
7. 多載與參數修飾詞 (ref, out, in)
C# 會把參數修飾詞算進方法簽章(也就是 void Foo(int a) 跟 void Foo(ref int a) 是不同方法,可以同時存在):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
沒用 ref 會叫到第一個,有用 ref 會叫到第二個:
int n = 5;
SetValue(n); // n 還是 5(值被複製)
SetValue(ref n); // n 變成 100
8. 圖解:什麼是多載
+----------+
| MyClass |
+----------+
|
| (方法片段)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
如果用程式碼來看:
// 呼叫多載的 Foo() 方法:
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. 範例:在我們的應用程式多載方法
來繼續我們的學習專案,幫動物繼承樹加上多載方法。
public class Animal
{
public string Name { get; set; }
// 發出聲音的方法
public virtual void MakeSound()
{
Console.WriteLine("某種奇怪的聲音...");
}
// 多載方法:指定音量的聲音
public void MakeSound(int volume)
{
Console.WriteLine($"動物發出 {volume} 分貝的聲音。");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪!");
}
// 多載方法:指定音量的汪汪
public void MakeSound(int volume)
{
Console.WriteLine($"汪!(音量: {volume} 分貝)");
}
}
可以這樣呼叫:
Dog rex = new Dog();
rex.MakeSound(); // 汪!
rex.MakeSound(75); // 汪!(音量: 75 分貝)
注意:在子類別 (Dog) 裡我們多載了 MakeSound(int volume),所以現在有兩個版本:有參數跟沒參數。
10. 多載常見錯誤
錯誤一:只靠回傳型態多載。
這不行——回傳型態不算在方法簽章裡。多載必須靠輸入參數的數量或型態,不是 void 或 int。
錯誤二:模糊多載讓編譯器抓狂。
參數數量一樣、型態很像(例如 int 跟 double)會讓編譯器搞不清楚。像 Print(int a, double b) 跟 Print(double a, int b),呼叫 Print(1, 1) 就會報模糊錯誤。
錯誤三:params 跟其他多載衝突。
用 params 的方法可能會搶走本來要給其他多載的呼叫。如果型態一樣,編譯器可能選錯版本。
錯誤四:忘了 ref 跟 out 也算簽章。
Do(ref int x) 跟 Do(out int x) 是不同多載。如果沒注意,很容易叫錯版本。
GO TO FULL VERSION