1. 引言
假设我们有个动物园动物管理的应用(就是我们前几节课一直在用的那个例子)。我们写了个基类Animal,里面有个MakeSound方法,这样所有动物都能“发出”某种声音。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音。");
}
}
但很快你会发现,狮子和鹦鹉的叫声完全不一样。通用的“动物发出声音”根本不合适。这时候方法重写就派上用场了。你需要让每个子类都能有自己的“声音”!
2. 语法:override怎么用
基本规则
- 要重写方法,基类里必须用virtual(或者abstract)标记它。
- 在派生类里,用override关键字修饰同样签名的方法。
例子:狮子和鹦鹉
public class Lion : Animal
{
public override void MakeSound()
{
Console.WriteLine("嗷呜呜呜!");
}
}
public class Parrot : Animal
{
public override void MakeSound()
{
Console.WriteLine("傻瓜鹦鹉!");
}
}
现在,如果你创建一个动物数组,然后对每个调用MakeSound,每个动物都会“说”自己的话:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Animal()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
// 输出:
// 嗷呜呜呜!
// 傻瓜鹦鹉!
// 动物发出声音。
3. 虚方法表(v-table)
当你用virtual和override时,编译器会为类生成一个特殊的“虚方法表”(v-table)。
通过基类引用调用方法时,CLR会查表:这个方法有没有被子类重写?如果有——就用子类的实现。
这就是“晚绑定”(late binding)的魔法,也叫动态多态。
4. 一起用起来:继续完善我们的应用
再加一个类:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
然后在我们的动物管理小程序里用它:
Animal[] zoo = new Animal[]
{
new Lion(),
new Parrot(),
new Dog()
};
foreach (var animal in zoo)
{
animal.MakeSound();
}
现在代码的可扩展性和可维护性都大大提升了,想加新动物也超方便!
5. 重写属性和带返回值的override
不只是方法可以重写。你也可以重写虚属性和索引器。
public class Animal
{
public virtual string Name { get; set; } = "动物";
}
public class Lion : Animal
{
public override string Name { get; set; } = "狮子";
}
这样可以很方便地为不同种类补充专属信息。
6. 用base调用基类实现
有时候你想扩展基类方法的行为,而不是完全替换它。
这时可以在重写方法里用base关键字调用基类实现。
public class Parrot : Animal
{
public override void MakeSound()
{
base.MakeSound(); // "动物发出声音。"
Console.WriteLine("傻瓜鹦鹉!");
}
}
这样鹦鹉会先发出通用的“动物园”声音,然后再来一句自己的“傻瓜鹦鹉!”。
7. 实际应用场景:哪里用得上
懂得override机制,在任何稍微复杂点的C#项目里都很重要,尤其是用到OOP的时候。
- 我们动物园的动物模型? 已经用过了。
- 自定义UI控件: 你可以重写标准的可视化方法,加上自己的逻辑。
- 灵活的业务逻辑: 可以在基类里写“骨架”,具体实现交给子类。
- 测试和mock: 通过继承很容易“替换”方法做单元测试。
- 插件和扩展: 接口或抽象基类,多种实现——全靠正确的重写机制才能跑起来。
8. sealed override和虚方法链
如果你不想让别人再往下重写你已经重写过的方法,可以用sealed override:
public class Base
{
public virtual void Foo() { }
}
public class Middle : Base
{
public sealed override void Foo() { }
}
public class Last : Middle
{
public override void Foo() {} // 报错——不能再重写了!
}
这样可以在需要的时候“封死”重写链,保证系统的正确性。
9. 新方法(new关键字)
有时候你想在子类里声明一个同名同参数的方法,但基类不是virtual的。
这时可以用new关键字——不过这不是多态,只是“遮盖”:
public class Animal
{
public void MakeSound()
{
Console.WriteLine("我是动物!");
}
}
public class Cat : Animal
{
public new void MakeSound()
{
Console.WriteLine("我是猫咪!");
}
}
Animal animal = new Cat();
animal.MakeSound(); // "我是动物!"
Cat cat = new Cat();
cat.MakeSound(); // "我是猫咪!"
这里调用时变量的类型说了算!所以要实现真正的动态多态,还是得用virtual + override组合。
10. 重写方法时的常见错误
错误1: 试图重写一个没用virtual、abstract或override声明的方法。
C#里不能重写普通方法。如果基类方法没加特殊修饰符(virtual、abstract或override),你在子类里写override会编译报错。
错误2: 方法签名不一致。
要想方法真的被重写,名字、返回类型和参数都得和基类方法一模一样。哪怕参数类型不一样,也只是新方法,不是重写。
错误3: 忘了加override修饰符。
如果你写了和基类同签名的方法,但没写override,编译器不会当成重写。这叫方法隐藏(后面会讲)。这时通过基类变量调用方法会得到意外结果——其实是基类方法被调用了,而不是你的。
GO TO FULL VERSION