继承

继承是一种“基于已有类型创建更具体类型”的方式。派生类(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 // 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(); // Woof!
cat.Speak(); // Meow!

继承是为了:

  1. 表达真实语义上的“是一个”(is-a)关系 Dog,Cat都属于 Animal。让类型体系与业务概念对齐,提高代码可读性与可推理性。
  2. 支持子类型多态(Polymorphism) 通过 virtual/abstract/override,使调用者只依赖基类/接口即可使用不同派生实现(开闭原则:对扩展开放,对修改封闭)。
  3. 复用通用实现与状态 基类集中放置公共字段、受保护方法、默认逻辑等,减少重复代码。
  4. 建立“扩展点” 通过 virtual/abstract 成员为用户提供可重写的行为。
  5. 简化调用端心智负担 调用端只需面向基类/接口编程,无需关心具体派生类型的细节(依赖倒置&里氏替换原则)。
  6. 封装变化点与稳定点 稳定不变的部分放基类;变化(可定制)部分暴露为抽象方法或虚方法。
  7. 形成类型层次增强“自描述性” 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(); // Woof!
a2.Move(); // The bird flies.

要点:

  • 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.");
}
}

// 使用
// var a = new Animal("X"); // 错:抽象类不能被实例化
Animal d = new Dog("Buddy");
d.Speak(); // Woof!

何时选用抽象?

  • 当基类只负责“定义规则/约定”,并且你不希望出现“默认实现”时(即:每个子类都必须给出自己的版本)。

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
{
// 隐藏(不推荐):Cat 的 Speak 隐藏了基类版本,但通过 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
{
// 同名 Feed,不同的参数签名
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); // 选中 Feed(Dog, int)
Feeder.Feed(dog, "Beef"); // 选中 Feed(Dog, string)
Feeder.Feed(cat, 80); // 选中 Feed(Cat, int)

要点:

  • 编译器在“看得到”的参数类型和数量基础上做决定。
  • 这跟对象的“实际运行时类型如何”无关。

一个容易混淆的小例子(重载选择在编译期,重写分派在运行期):

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");
// 编译期:根据“变量的静态类型”选择重载 → 选中 Call(Animal)
// 运行期:Speak() 是虚方法 → 分派到 Dog.Speak()
Caller.Call(a); // 输出:Call(Animal): Woof!

说明:

  • 重载的“哪个方法被调用”在编译期就确定了(因为 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();
}

要点:

  • 使用方只依赖“基类或抽象类”的约定(如 AnimalSpeak())。
  • 新增一个动物子类,只要实现约定的方法,现有调用代码无需修改即可“自动适配”。

3. 两类多态如何区分与选择?

  • 你在“用不同的参数签名,表达同一种意图的不同输入形式”时,用“编译时多态(重载)”更自然。

    例如 Feed(Dog, int)Feed(Dog, string) 都是在“喂狗”,只是在意输入的单位不同。

  • 你在“希望调用方只关心抽象类型,而实际行为由子类差异来决定”时,用“运行时多态(虚方法/抽象方法 + override)”更合适。

    例如 Animal.Speak(),不同动物发出不同声音。

小贴士:

  • “重载 vs 重写”:
    • 重载(overload):同名、不同参数;发生在“同一个类里”或同一继承层级中;编译期决定。
    • 重写(override):同名、同参数;发生在“子类改写基类虚方法”;运行期决定。
  • C# 还支持“运算符重载”等属于编译时多态的形式,此处暂不涉及。

小结

  • 继承能把“共同点”抽到基类里,减少重复。
  • virtual + override 让同名方法在不同子类有不同表现(运行时多态)。
  • 当必须强制子类给实现时,用 abstract 抽象类/成员。
  • base 可以复用或扩展基类逻辑。
  • 避免用 new 隐藏基类方法,优先用 override
  • 多态分为编译时多态(重载)与运行时多态(继承/重写),记住“重载看参数签名(编译期),重写看实际对象(运行期)”。