继承
继承是一种“基于已有类型创建更具体类型”的方式。派生类(Derived Class)会自动拥有基类(Base Class)的属性和方法,并且可以在此基础上新增能力或改写行为。它常用来表达“是一个”的关系,比如“狗是一种动物”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Animal { public string Name { get; set; } }
public class Dog : Animal { }
var d = new Dog(); d.Name = "Foo"; Console.WriteLine(d.Name);
|
1. 为什么需要继承?
当多个类型有“共同点”时,继承可以把这些公共部分提取到一个“更通用”的类型中,减少重复代码。比如“狗”和“猫”都是“动物”,都有名字、年龄,也都会发出声音。
不使用继承的写法会有大量重复代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Dog { public string Name { get; } public int Age { get; }
public Dog(string name, int age) { Name = name; Age = age; }
public void Speak() { Console.WriteLine("Woof!"); } }
public class Cat { public string Name { get; } public int Age { get; }
public Cat(string name, int age) { Name = name; Age = age; }
public void Speak() { Console.WriteLine("Meow!"); } }
|
使用继承,把“共同点”放到基类 Animal 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class Animal { public string Name { get; } public int Age { get; }
public Animal(string name, int age) { Name = name; Age = age; }
public virtual void Speak() { Console.WriteLine("(Some animal sound)"); } }
public class Dog : Animal { public Dog(string name, int age) : base(name, age) { }
public override void Speak() { Console.WriteLine("Woof!"); } }
public class Cat : Animal { public Cat(string name, int age) : base(name, age) { }
public override void Speak() { Console.WriteLine("Meow!"); } }
var dog = new Dog("Buddy", 3); var cat = new Cat("Kitty", 2); dog.Speak(); cat.Speak();
|
继承是为了:
- 表达真实语义上的“是一个”(is-a)关系
Dog,Cat都属于 Animal。让类型体系与业务概念对齐,提高代码可读性与可推理性。
- 支持子类型多态(Polymorphism) 通过
virtual/abstract/override,使调用者只依赖基类/接口即可使用不同派生实现(开闭原则:对扩展开放,对修改封闭)。
- 复用通用实现与状态 基类集中放置公共字段、受保护方法、默认逻辑等,减少重复代码。
- 建立“扩展点” 通过
virtual/abstract 成员为用户提供可重写的行为。
- 简化调用端心智负担 调用端只需面向基类/接口编程,无需关心具体派生类型的细节(依赖倒置&里氏替换原则)。
- 封装变化点与稳定点 稳定不变的部分放基类;变化(可定制)部分暴露为抽象方法或虚方法。
- 形成类型层次增强“自描述性” IDE 类型提示、文档、架构图更清晰:层次结构本身就是领域模型的一部分。
2. 基本语法:基类与派生类
- 定义基类:
class Animal { ... }
- 定义派生类(继承自基类):
class Dog : Animal { ... }
- 显式调用基类构造函数:在子类构造函数后使用
: base(...)
示例(包含构造函数链式调用):
1 2 3 4 5 6 7 8 9 10
| public class Animal { public string Name { get; } public Animal(string name) => Name = name; }
public class Dog : Animal { public Dog(string name) : base(name) { } }
|
3. 虚方法(virtual)与重写(override)
- 在基类里把可能被“改写”的方法标记为
virtual。
- 在子类里使用
override 来“重写”它,以实现更精准的行为。
让不同动物发出不同声音:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class Animal { public string Name { get; } public Animal(string name) => Name = name;
public virtual void Speak() { Console.WriteLine("(Some animal sound)"); }
public virtual void Move() { Console.WriteLine("The animal moves."); } }
public class Dog : Animal { public Dog(string name) : base(name) { }
public override void Speak() { Console.WriteLine("Woof!"); }
public override void Move() { Console.WriteLine("The dog runs."); } }
public class Bird : Animal { public Bird(string name) : base(name) { }
public override void Speak() { Console.WriteLine("Tweet!"); }
public override void Move() { Console.WriteLine("The bird flies."); } }
Animal a1 = new Dog("Buddy"); Animal a2 = new Bird("Sky");
a1.Speak(); a2.Move();
|
要点:
virtual + override 让“同一个方法名”,在不同子类里表现不同的行为。
- 这样写,使用者只需要把对象当作“Animal”看待,实际效果由“具体的子类”决定。
4. 使用 base 调用基类实现
有时候你想在子类里“扩展”而不是完全替换基类的行为,可以先调用 base.方法() 再补充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Animal { public virtual void Move() { Console.WriteLine("The animal moves."); } }
public class Dog : Animal { public override void Move() { base.Move(); Console.WriteLine("The dog runs faster!"); } }
|
5. 抽象类(abstract)与抽象成员
当你希望“基类只定义规范,不给出具体实现”时,用 abstract。
- 抽象类:用
abstract class 声明,不能直接 new。
- 抽象成员:用
abstract 声明的方法/属性没有方法体,必须在子类里 override 实现。
强制每种动物都实现自己的 Speak方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| public abstract class Animal { public string Name { get; } protected Animal(string name) => Name = name;
public abstract void Speak();
public virtual void Move() { Console.WriteLine("The animal moves."); } }
public class Dog : Animal { public Dog(string name) : base(name) { }
public override void Speak() { Console.WriteLine("Woof!"); }
public override void Move() { Console.WriteLine("The dog runs."); } }
public class Fish : Animal { public Fish(string name) : base(name) { }
public override void Speak() { Console.WriteLine("(Fish are quiet...)"); }
public override void Move() { Console.WriteLine("The fish swims."); } }
Animal d = new Dog("Buddy"); d.Speak();
|
何时选用抽象?
- 当基类只负责“定义规则/约定”,并且你不希望出现“默认实现”时(即:每个子类都必须给出自己的版本)。
6. 选择虚方法还是抽象成员?
- 如果基类能给出一个“有意义”的默认实现,但允许子类改写:用
virtual。
- 如果基类不给出默认实现,且必须强制子类实现:用
abstract。
1 2 3 4 5 6 7 8
| public abstract class Animal { public abstract void Speak();
public virtual void Move() => Console.WriteLine("The animal moves."); }
|
7. 方法“重写”与“隐藏”不是一回事
如果在子类里写了一个“同名、同参数”的方法,但没有 override,而是用了 new,那是“隐藏”。非特殊情况下不建议使用隐藏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Animal { public virtual void Speak() => Console.WriteLine("(Some animal sound)"); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("Woof!"); }
public class Cat : Animal { public new void Speak() => Console.WriteLine("Meow!"); }
|
一般情况下,子类改写基类行为时请使用 override,不要用 new。
8. 一个更完整的小例子
把上面的点串起来,做一个简单的“动物园”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| public abstract class Animal { public string Name { get; } protected Animal(string name) => Name = name;
public abstract void Speak();
public virtual void Move() { Console.WriteLine($"{Name} moves."); } }
public class Dog : Animal { public Dog(string name) : base(name) { }
public override void Speak() => Console.WriteLine($"{Name}: Woof!"); public override void Move() => Console.WriteLine($"{Name} runs."); }
public class Bird : Animal { public Bird(string name) : base(name) { }
public override void Speak() => Console.WriteLine($"{Name}: Tweet!"); public override void Move() => Console.WriteLine($"{Name} flies."); }
public static class Zoo { public static void Show(Animal animal) { animal.Speak(); animal.Move(); } }
var animals = new List<Animal> { new Dog("Buddy"), new Bird("Sky") };
foreach (var a in animals) { Zoo.Show(a); }
|
输出:
- Buddy: Woof!
- Buddy runs.
- Sky: Tweet!
- Sky flies.
要点:
- 调用方只认识“Animal”,不用关心具体类型。
- 每个子类负责自己的差异,实现灵活扩展。
多态
(Polymorphism):同名,不同行为。
多态指的是:用“同一个调用方式”(同一个方法名),针对不同的对象或参数,表现出“不同的实际行为”。在 C# 中常见的多态主要有两大类:
- 编译时多态(也叫静态多态、重载多态)
- 运行时多态(也叫动态多态、继承多态/子类型多态)
1. 编译时多态(重载多态)
- 在“编译阶段”由编译器根据参数列表来决定调用哪个方法。
- 代表形式:方法重载(同名方法,不同参数个数或类型)。
方法重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static class Feeder { public static void Feed(Dog dog, int grams) => Console.WriteLine($"Feed {dog.Name} {grams}g of dog food.");
public static void Feed(Dog dog, string specialFood) => Console.WriteLine($"Feed {dog.Name} special food: {specialFood}.");
public static void Feed(Cat cat, int grams) => Console.WriteLine($"Feed {cat.Name} {grams}g of cat food."); }
var dog = new Dog("Buddy"); var cat = new Cat("Kitty");
Feeder.Feed(dog, 100); Feeder.Feed(dog, "Beef"); Feeder.Feed(cat, 80);
|
要点:
- 编译器在“看得到”的参数类型和数量基础上做决定。
- 这跟对象的“实际运行时类型如何”无关。
一个容易混淆的小例子(重载选择在编译期,重写分派在运行期):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public static class Caller { public static void Call(Animal a) { Console.Write("Call(Animal): "); a.Speak(); }
public static void Call(Dog d) { Console.Write("Call(Dog): "); d.Speak(); } }
Animal a = new Dog("Buddy");
Caller.Call(a);
|
说明:
- 重载的“哪个方法被调用”在编译期就确定了(因为 a 的静态类型是 Animal)。
- 但具体执行哪个
Speak() 实现是在运行期决定的(因为对象实际是 Dog)。
2. 运行时多态(继承多态/子类型多态)
特点:
- 通过
virtual/override(或 abstract/override)实现。
- 方法的“最终实现”在运行时,根据对象的“实际类型”来决定。
前文的“动物园”就是典型的运行时多态:
1 2 3 4 5 6 7 8 9 10 11 12
| var animals = new List<Animal> { new Dog("Buddy"), new Bird("Sky"), new Fish("Nemo") };
foreach (var a in animals) { a.Speak(); a.Move(); }
|
要点:
- 使用方只依赖“基类或抽象类”的约定(如
Animal 的 Speak())。
- 新增一个动物子类,只要实现约定的方法,现有调用代码无需修改即可“自动适配”。
3. 两类多态如何区分与选择?
你在“用不同的参数签名,表达同一种意图的不同输入形式”时,用“编译时多态(重载)”更自然。
例如 Feed(Dog, int) 与 Feed(Dog, string) 都是在“喂狗”,只是在意输入的单位不同。
你在“希望调用方只关心抽象类型,而实际行为由子类差异来决定”时,用“运行时多态(虚方法/抽象方法 + override)”更合适。
例如 Animal.Speak(),不同动物发出不同声音。
小贴士:
- “重载 vs 重写”:
- 重载(overload):同名、不同参数;发生在“同一个类里”或同一继承层级中;编译期决定。
- 重写(override):同名、同参数;发生在“子类改写基类虚方法”;运行期决定。
- C# 还支持“运算符重载”等属于编译时多态的形式,此处暂不涉及。
小结
- 继承能把“共同点”抽到基类里,减少重复。
- 用
virtual + override 让同名方法在不同子类有不同表现(运行时多态)。
- 当必须强制子类给实现时,用
abstract 抽象类/成员。
- 用
base 可以复用或扩展基类逻辑。
- 避免用
new 隐藏基类方法,优先用 override。
- 多态分为编译时多态(重载)与运行时多态(继承/重写),记住“重载看参数签名(编译期),重写看实际对象(运行期)”。