1. 入门
如果你现在一听到重写就有点慌,别怕——其实很简单还超好用!方法重写就是让你在子类里把父类的方法行为换成自己的。这样代码就变得灵活、可扩展,还能应对真实世界,比如每只动物都不想只是“发出某种声音”。
想让方法能被重写,父类要用关键字virtual标记它。子类想换实现,就用关键字override。
例子
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("某种通用的动物声音...");
}
}
现在在Dog类里:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
- 父类用virtual。
- 子类用override。
- 方法签名(名字、返回类型、参数)必须一样。
可视化图解
2. 实际演示
来看看重写到底怎么用。我们创建几个动物,听听它们的声音:
Animal pet1 = new Animal { Name = "无名动物" };
Dog pet2 = new Dog { Name = "巴博斯" };
pet1.MakeSound(); // 输出:某种通用的动物声音...
pet2.MakeSound(); // 输出:汪汪!
现在来点难的:
如果我们把Dog存到Animal类型的变量里会怎样?
Animal pet3 = new Dog { Name = "沙里克" };
pet3.MakeSound(); // ???
你觉得会发生什么?
答案:会输出“汪汪!”
因为虽然变量类型是Animal,但它“指向”的其实是Dog对象,所以会调用重写后的方法!
这就是动态(或延迟)绑定的魔法。
3. 重写时用base关键字
有时候你不想完全替换方法实现,只想扩展一下——比如加点自己的东西,再执行原来的逻辑。这时就用base关键字。它能让你调用父类的方法版本。
public class Cat : Animal
{
public override void MakeSound()
{
base.MakeSound(); // 调用父类实现
Console.WriteLine("喵!");
}
}
调用这个方法时,先会输出“某种通用的动物声音...”,然后“喵!”
4. 重写时方法选择的原理
想直观理解“底层”发生了啥,可以想象下面这张表(虚方法分派):
| 变量类型 | 对象类型 | 会调用哪个方法 |
|---|---|---|
| Animal | Animal | Animal.MakeSound |
| Animal | Dog | Dog.MakeSound |
| Animal | Labrador | Labrador.MakeSound |
| Dog | Labrador | Labrador.MakeSound |
| Dog | Dog | Dog.MakeSound |
核心规则:
变量类型只对编译器有用,运行时看的是实际对象类型(就是你用new创建的那个)。
这个机制叫动态(或延迟)绑定——多态就是靠它实现的(下节课会详细讲!)。
为啥要重写方法
- 在GUI框架里:你有个窗口基类,重写方法来画具体控件。
- 在游戏引擎里:有个Enemy基类,子类实现不同的行为。
- 在单元测试里:可以为方法造“桩”(stubs, mocks)。
现代.NET框架大量用这个机制搞事件、模板代码、继承配置,甚至对象序列化(比如虚属性)。
5. 用new关键字隐藏方法
我们已经知道重写方法要用virtual/override这对组合。但C#还有一个和继承方法有关的修饰符——那就是new。
new有啥用?
new用在子类里声明和父类同签名的方法,但你不想重写虚方法,而是隐藏(遮住)父类的方法。
- 这不是重写,是隐藏。
- 这种方法是按变量类型调用的,不看实际对象类型(没有动态多态!)。
- 如果你“无意”隐藏了方法但没写new,编译器会警告你。
例子:override和new的区别
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物发出某种声音...");
}
}
public class Dog : Animal
{
// 隐藏父类方法(不是重写)
public new void MakeSound()
{
Console.WriteLine("这不是override!只是Dog的方法。");
}
}
现在看看行为:
Animal a = new Dog();
Dog d = new Dog();
a.MakeSound(); // "动物发出某种声音..."
d.MakeSound(); // "这不是override!只是Dog的方法。"
- 变量类型是Dog——会调用Dog的方法。
- 变量类型是Animal,即使里面装的是Dog——也会调用Animal的方法!
6. 反馈和实现细节
刚学编程时经常会遇到“明明重写了方法,怎么还是老样子”的困惑。通常原因很简单:父类没写virtual,或者子类方法用了new而不是override。第二种尤其坑——如果用父类变量调用,会走父类方法,不是重写的那个。所以一定要注意关键字用对了。
除了语法错误,新手有时还想在重写时改返回类型。比如父类方法返回object,子类想返回string。这样不行:方法签名必须完全一样。
对比表:override vs new
| 特点 | override | new |
|---|---|---|
| 机制 | 重写虚方法 | 隐藏父类方法 |
| 延迟绑定 | 有——支持动态多态 | 没有——按变量类型 |
| 要求base里有... | virtual、abstract或已经override | 不需要 |
| 推荐使用 | 几乎总是 | 只有特殊情况 |
7. 重写方法和继承链
有了长继承链,重写才真正有意思:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物在做点啥...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
public class Labrador : Dog
{
public override void MakeSound()
{
Console.WriteLine("我是拉布拉多:汪-汪!");
}
}
如果我们写:
Animal pet = new Labrador();
pet.MakeSound(); // => "我是拉布拉多:汪-汪!"
C#总是选“最深”的虚方法实现,也就是对象本身的那个。
8. 重写方法的常见错误
世界不完美,学生(还有老司机!)有时会踩坑。我们来提前避开最常见的“雷”:
1. 父类忘了virtual
public class Animal
{
public void MakeSound() { ... } // 没有'virtual'
}
public class Dog : Animal
{
// 编译错误!不能重写。
public override void MakeSound()
{
Console.WriteLine("汪!");
}
}
C#会直接报错:'Dog.MakeSound()': 没有可重写的方法
2. 签名不一致
一定要保证方法名、返回类型和参数都一样:
public class Animal
{
public virtual void MakeSound() { ... }
}
public class Dog : Animal
{
// 错误:签名不同(比如多了参数)
public override void MakeSound(string sound)
{
Console.WriteLine(sound);
}
}
3. 没必要别用new代替override
new关键字可以隐藏父类方法,但这不是重写,也不支持动态多态。一般没特殊需求别用它。
GO TO FULL VERSION