1. 入门
现实生活中很多操作就像多功能瑞士军刀:同一个命令可以用不同的工具来完成。比如,想象一下ATM机:你插卡——ATM让你输密码;你输手机号——ATM等你输入短信验证码。动作是一个——“验证用户”,但方式不一样。
编程里我们经常遇到类似的情况:看起来要做同一个操作,但数据类型或者参数数量可能不一样。比如,我们的方法要给人或者动物打招呼,或者要把2个、3个甚至10个整数加起来。
当然,你可以把方法名字都取不一样:SumTwo、SumThree、SumArray。但程序员都很懒(不是说懒是进步的动力嘛)。而且这样代码也不好读。
方法重载
方法重载就是让同一个方法名能处理不同参数组合的一种方式,方法名不变。这其实是多态的一种,但和继承没关系。
方法重载就是你可以在一个类(或者结构体)里写多个同名方法,但参数列表(类型、数量、顺序)不一样。
方法签名
C#里的方法签名就是方法名加上参数类型和顺序。返回类型不算在签名里! 这经常会让人踩坑(后面会说)。
2. 重载实战:简单例子
我们来写个Greeter类,可以用不同方式打招呼:只用名字、用名字和年龄,或者啥参数都没有。
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. 方法重载常见错误
错误1:只靠返回类型重载方法。
这是不行的——返回类型不算在方法签名里。重载必须靠输入参数的数量或类型,不是void还是int。
错误2:歧义重载让编译器懵逼。
参数数量一样、类型很接近的重载(比如int和double)会让编译器分不清。例子:Print(int a, double b)和Print(double a, int b),你写Print(1, 1)就会报歧义错误。
错误3:params和其他重载冲突。
带params的方法可能会抢走本来该给别的重载的调用。如果类型一样,编译器可能选错你想要的方法。
错误4:忘了ref和out算签名一部分。
Do(ref int x)和Do(out int x)是两个不同的重载。如果你没注意,很容易搞混,调用错方法。
GO TO FULL VERSION