CSharp(二十四)类(Class)继承
目录
- 什么是继承?—— 生活例子秒懂
- 继承的基本语法
- 用动物例子深入理解
- 构造函数在继承中的执行顺序
- virtual 和 override —— 方法重写
- base 关键字详解
- 多层继承:爷爷 → 爸爸 → 儿子
- C# 继承的重要规则
- 继承 vs 组合 —— 何时用继承?
- 综合实战案例
- 常见错误与自查清单
- 小结与记忆口诀
1. 什么是继承?—— 生活例子秒懂
1.1 生活中的继承
先忘掉代码,想想现实生活:
你的父母有房子、车子,你去世后这些财产会传给你——这就是"继承"。
在编程里,继承也是类似的意思:
- 父类(基类):把"财产"(字段、属性、方法)准备好
- 子类(派生类):自动拥有这些"财产",不用重新写一遍
- 子类还可以添加自己的新东西,甚至改造父类留下的东西
1.2 一个直观比喻
想象你在开发一个宠物店管理系统:
动物(Animal)
├── 狗(Dog)
├── 猫(Cat)
└── 鸟(Bird)
所有动物都有:名字、年龄、吃东西。这些共性写在 Animal 里。
狗有自己的特点:品种、汪汪叫、捡球。这些写在 Dog 里,同时自动拥有 Animal 的所有内容。
这就是继承:把共同的东西抽到父类,子类只管自己的特色。
1.3 继承解决了什么问题?
| 没有继承(各写各的) | 有继承(统一管理) |
|---|---|
Dog 类里写 Name、Age、Eat |
写在 Animal 里一次 |
Cat 类里又写一遍 Name、Age、Eat |
Dog、Cat、Bird 全部自动拥有 |
| 修改 "吃东西" 逻辑要改 3 个文件 | 只改 Animal 一个地方,全部生效 |
新增 Bird 又要从头写 Name、Age、Eat |
新增 Bird 只需写飞行的代码 |
核心思想:DRY 原则 —— Don't Repeat Yourself(别重复自己)
2. 继承的基本语法
2.1 语法格式
// 父类(基类)
public class 父类名
{
// 公共的字段、属性、方法
}
// 子类(派生类)—— 用冒号 ":" 表示继承
public class 子类名 : 父类名
{
// 子类独有的字段、属性、方法
}
就这么简单!一个冒号 : 就建立了继承关系。
2.2 最简单的例子
// 父类:人
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"大家好,我叫 {Name}");
}
}
// 子类:学生(继承 Person)
public class Student : Person
{
public string StudentId { get; set; }
public void Study()
{
Console.WriteLine($"{Name} 正在学习...");
}
}
// 子类:老师(继承 Person)
public class Teacher : Person
{
public string Subject { get; set; }
public void Teach()
{
Console.WriteLine($"{Name} 老师正在教 {Subject}");
}
}
使用效果:
Student stu = new Student();
stu.Name = "小明"; // ← 这是从 Person 继承来的!
stu.Age = 15; // ← 这也是从 Person 继承来的!
stu.StudentId = "S001"; // ← 这是 Student 自己的
stu.SayHello(); // ← 继承来的方法,输出:大家好,我叫 小明
stu.Study(); // ← Student 自己的方法
Teacher teacher = new Teacher();
teacher.Name = "王老师"; // ← 从 Person 继承来的
teacher.Subject = "数学"; // ← Teacher 自己的
teacher.SayHello(); // ← 继承来的方法
teacher.Teach(); // ← Teacher 自己的方法
2.3 子类到底"继承"了什么?
子类自动获得父类的以下成员(前提是访问修饰符允许):
- ✅
public字段、属性、方法 - ✅
protected字段、属性、方法(后面会讲) - ✅
internal字段、属性、方法(同项目内) - ❌
private成员 —— 不继承! - ❌ 构造函数 —— 不继承!(但子类构造函数会调用父类构造函数)
记住:
private= 私房钱,连亲儿子也拿不到。
3. 用动物例子深入理解
这是本教程最核心的例子,请仔细阅读每一行注释:
// ==================== 父类:动物 ====================
public class Animal
{
// 字段(用属性封装)
public string Name { get; set; } // 名字 —— 所有动物都有
public int Age { get; set; } // 年龄 —— 所有动物都有
// 构造函数
public Animal()
{
Console.WriteLine("① Animal 无参构造函数执行");
}
public Animal(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine("① Animal 带参构造函数执行");
}
// 普通方法 —— 所有动物都会吃东西
public void Eat()
{
Console.WriteLine($"{Name} 正在吃东西~");
}
// 虚方法 —— 子类可以选择重写(改变行为)
public virtual void MakeSound()
{
Console.WriteLine("(某种动物的叫声)");
}
}
// ==================== 子类:狗 ====================
public class Dog : Animal
{
// 狗独有的属性
public string Breed { get; set; } // 品种
// 构造函数 —— 调用父类构造函数
public Dog() : base()
{
Console.WriteLine("② Dog 无参构造函数执行");
}
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
Console.WriteLine("② Dog 带参构造函数执行");
}
// 重写父类方法 —— 狗有自己的叫声
public override void MakeSound()
{
Console.WriteLine($"{Name}:汪汪汪!🐶");
}
// 狗独有的方法
public void FetchBall()
{
Console.WriteLine($"{Name} 飞奔去捡球!🎾");
}
}
// ==================== 子类:猫 ====================
public class Cat : Animal
{
public string FurColor { get; set; } // 毛色
public Cat(string name, int age, string furColor) : base(name, age)
{
FurColor = furColor;
}
// 猫也有自己的叫声
public override void MakeSound()
{
Console.WriteLine($"{Name}:喵喵喵~ 🐱");
}
// 猫独有的方法
public void ClimbTree()
{
Console.WriteLine($"{Name} 嗖地一下爬上了树!🌳");
}
}
// ==================== 子类:鸟 ====================
public class Bird : Animal
{
public double WingSpan { get; set; } // 翼展
public Bird(string name, int age, double wingSpan) : base(name, age)
{
WingSpan = wingSpan;
}
public override void MakeSound()
{
Console.WriteLine($"{Name}:叽叽喳喳~ 🐦");
}
public void Fly()
{
Console.WriteLine($"{Name} 展开 {WingSpan}cm 的翅膀飞走了~");
}
}
动手实验:运行这段代码
class Program
{
static void Main(string[] args)
{
Console.WriteLine("========== 创建一只狗 ==========");
Dog dog = new Dog("大黄", 3, "金毛");
Console.WriteLine("\n========== 狗的行为 ==========");
dog.Eat(); // 继承自 Animal
dog.MakeSound(); // Dog 重写后的版本
dog.FetchBall(); // Dog 独有的方法
Console.WriteLine("\n========== 创建一只猫 ==========");
Cat cat = new Cat("咪咪", 2, "白色");
Console.WriteLine("\n========== 猫的行为 ==========");
cat.Eat(); // 继承自 Animal(同样的代码!)
cat.MakeSound(); // Cat 重写后的版本
cat.ClimbTree(); // Cat 独有的方法
Console.WriteLine("\n========== 创建一只鸟 ==========");
Bird bird = new Bird("啾啾", 1, 25.5);
Console.WriteLine("\n========== 鸟的行为 ==========");
bird.Eat(); // 继承自 Animal
bird.MakeSound(); // Bird 重写后的版本
bird.Fly(); // Bird 独有的方法
}
}
/* ========== 程序输出 ==========
========== 创建一只狗 ==========
① Animal 带参构造函数执行
② Dog 带参构造函数执行
========== 狗的行为 ==========
大黄 正在吃东西~
大黄:汪汪汪!🐶
大黄 飞奔去捡球!🎾
========== 创建一只猫 ==========
① Animal 带参构造函数执行
========== 猫的行为 ==========
咪咪 正在吃东西~
咪咪:喵喵喵~ 🐱
咪咪 嗖地一下爬上了树!🌳
========== 创建一只鸟 ==========
① Animal 带参构造函数执行
========== 鸟的行为 ==========
啾啾 正在吃东西~
啾啾:叽叽喳喳~ 🐦
啾啾 展开 25.5cm 的翅膀飞走了~
*/
看出门道了吗?
Eat()只写了一次(在Animal里),但Dog、Cat、Bird都能用MakeSound()每个子类各写各的,体现出不同动物的特性- 每种动物还加了自己的独门绝技(
FetchBall、ClimbTree、Fly)
4. 构造函数在继承中的执行顺序
这是初学者最容易踩坑的地方!牢记一句话:
先有父亲,才有儿子。构造一个子类对象时,必须先构造好父类部分。
4.1 执行顺序图解
创建 new Dog() 时的执行顺序:
第一步:Animal 的字段初始化 ─┐
第二步:Animal 构造函数执行 ─┼── 父类部分先建好
第三步:Dog 的字段初始化 ─┐
第四步:Dog 构造函数执行 ─┼── 子类部分再建好
│
结果:一个完整的 Dog 对象诞生! ←┘
4.2 代码验证
public class GrandParent
{
public GrandParent()
{
Console.WriteLine("→ 爷爷的构造函数");
}
}
public class Parent : GrandParent
{
public Parent()
{
Console.WriteLine(" → 爸爸的构造函数");
}
}
public class Child : Parent
{
public Child()
{
Console.WriteLine(" → 儿子的构造函数");
}
}
// 执行:
Child c = new Child();
/* 输出:
→ 爷爷的构造函数
→ 爸爸的构造函数
→ 儿子的构造函数
*/
4.3 使用 : base() 选择父类构造函数
如果父类有多个构造函数,子类可以用 base 关键字指定调用哪一个:
public class Animal
{
public Animal()
{
Console.WriteLine("Animal 无参构造函数");
}
public Animal(string name)
{
Console.WriteLine($"Animal 带参构造函数:{name}");
}
}
public class Dog : Animal
{
// 不写 base → 默认调用父类的无参构造函数
public Dog()
{
Console.WriteLine("Dog 无参构造函数");
}
// base(name) → 调用父类的 Animal(string name) 构造函数
public Dog(string name) : base(name)
{
Console.WriteLine($"Dog 带参构造函数:{name}");
}
}
// 测试:
Dog d1 = new Dog();
/* 输出:
Animal 无参构造函数
Dog 无参构造函数
*/
Dog d2 = new Dog("大黄");
/* 输出:
Animal 带参构造函数:大黄
Dog 带参构造函数:大黄
*/
规则总结:
- 子类构造函数第一行一定是调用父类构造函数(显式或隐式)
- 不写
base()时,编译器自动添加: base()- 父类如果没有无参构造函数,子类必须显式调用
base(参数)
5. virtual 和 override —— 方法重写
这是继承的灵魂。
5.1 为什么需要 virtual 和 override?
看一个没有 virtual/override 的情况:
public class Animal
{
// 注意:没有 virtual 关键字
public void MakeSound()
{
Console.WriteLine("动物叫了一声");
}
}
public class Dog : Animal
{
// 注意:没有 override,用了 new(隐藏)
public new void MakeSound()
{
Console.WriteLine("汪汪汪!");
}
}
// ==== 问题来了 ====
Dog dog = new Dog();
dog.MakeSound(); // 输出:汪汪汪! ← 用 Dog 类型调用,正常
Animal animal = new Dog(); // 用 Animal 类型变量指向 Dog 对象
animal.MakeSound(); // 输出:动物叫了一声 ← 咦?明明是个狗啊!
这就是问题:当你用父类变量指向子类对象时,调用的还是父类的方法。
5.2 用 virtual + override 解决
public class Animal
{
// virtual = "我允许子类重新定义这个方法"
public virtual void MakeSound()
{
Console.WriteLine("动物叫了一声");
}
}
public class Dog : Animal
{
// override = "我要重新定义父类的这个方法"
public override void MakeSound()
{
Console.WriteLine("汪汪汪!");
}
}
// ==== 现在看看效果 ====
Dog dog = new Dog();
dog.MakeSound(); // 输出:汪汪汪!
Animal animal = new Dog(); // 父类变量,指向子类对象
animal.MakeSound(); // 输出:汪汪汪!✅ 对的!这才是多态!
5.3 一张表看懂三个关键字
| 关键字 | 放在哪 | 含义 |
|---|---|---|
virtual |
父类方法上 | "这个方法子类可以重写" |
override |
子类方法上 | "我重写父类的 virtual 方法" |
new |
子类方法上 | "我隐藏父类的方法(不是重写,是全新的" |
5.4 virtual vs new 对比实验
public class Father
{
public virtual void VirtualMethod()
{
Console.WriteLine("父类的 virtual 方法");
}
public void NormalMethod()
{
Console.WriteLine("父类的普通方法");
}
}
public class Son : Father
{
// override:真正重写
public override void VirtualMethod()
{
Console.WriteLine("子类 override 的方法");
}
// new:仅仅隐藏
public new void NormalMethod()
{
Console.WriteLine("子类 new 的方法");
}
}
// ==== 关键对比 ====
Son son = new Son();
son.VirtualMethod(); // 输出:子类 override 的方法
son.NormalMethod(); // 输出:子类 new 的方法
Father father = son; // 用父类类型指向同一个对象
father.VirtualMethod(); // 输出:子类 override 的方法 ← 多态!调子类的
father.NormalMethod(); // 输出:父类的普通方法 ← 没有多态!调父类的
一句话总结:
virtual+override→ 儿子真正"取代"了父亲的方法(多态)new→ 儿子只是"挡住"了父亲的方法,父亲的方法还在那里
6. base 关键字详解
base 代表父类,this 代表自己。base 有两大用途:
6.1 用途一:调用父类的构造函数
public class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
Console.WriteLine($"Animal 创建:{name}");
}
}
public class Dog : Animal
{
public string Breed { get; set; }
// base(name) → 先调用 Animal(name),再执行自己的
public Dog(string name, string breed) : base(name)
{
Breed = breed;
Console.WriteLine($"Dog 创建:{name},品种:{breed}");
}
}
6.2 用途二:调用父类的方法
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("(基础叫声)");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
// 先执行父类的逻辑...
base.MakeSound(); // 调用 Animal.MakeSound()
// 再加上自己的逻辑
Console.WriteLine("汪汪汪!");
}
}
// 使用:
Dog dog = new Dog();
dog.MakeSound();
/* 输出:
(基础叫声)
汪汪汪!
*/
6.3 base 使用场景
public class Logger
{
public virtual void Log(string message)
{
// 基础的日志格式:加上时间戳
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
}
public class FileLogger : Logger
{
public override void Log(string message)
{
// 复用父类的时间戳格式化
base.Log(message);
// 增加自己的功能:写入文件
File.AppendAllText("log.txt", message + Environment.NewLine);
}
}
7. 多层继承:爷爷 → 爸爸 → 儿子
C# 支持多层继承(一个链条),但不支持多继承(一个儿子多个爸爸)。
7.1 多层继承示例
// 第一层:生物(最基础的)
public class LivingBeing
{
public string Name { get; set; }
public void Breathe()
{
Console.WriteLine($"{Name} 在呼吸...");
}
}
// 第二层:动物(继承生物,加上移动能力)
public class Animal : LivingBeing
{
public int Age { get; set; }
public void Move()
{
Console.WriteLine($"{Name} 在移动...");
}
}
// 第三层:哺乳动物(继承动物,加上哺乳特性)
public class Mammal : Animal
{
public void FeedMilk()
{
Console.WriteLine($"{Name} 在哺乳幼崽...");
}
}
// 第四层:狗(继承哺乳动物,加上自己的特性)
public class Dog : Mammal
{
public string Breed { get; set; }
public void Bark()
{
Console.WriteLine($"{Name}:汪汪!");
}
}
// 测试:一个 Dog 对象拥有的所有能力
Dog dog = new Dog() { Name = "大黄", Age = 3, Breed = "金毛" };
dog.Breathe(); // 来自 LivingBeing(太爷爷)
dog.Move(); // 来自 Animal(爷爷)
dog.FeedMilk(); // 来自 Mammal(爸爸)
dog.Bark(); // 自己的
Console.WriteLine($"\n大黄的继承链:");
Console.WriteLine($"Dog → Mammal → Animal → LivingBeing → Object");
// C# 中所有类最终都继承自 System.Object
7.2 继承链条图
Object(C# 中所有类的最终祖先)
↑
LivingBeing(名字、呼吸)
↑
Animal(年龄、移动)
↑
Mammal(哺乳)
↑
Dog(品种、汪汪叫)
8. C# 继承的重要规则
8.1 规则一:单继承
一个类只能有一个直接父类。
// ❌ 错误!C# 不支持多继承
public class Child : Father, Mother // 编译错误!
{
}
// ✅ 正确:单继承
public class Child : Father
{
}
如果你需要"多继承"的效果,使用接口(后面会学):
// ✅ 通过接口实现"多继承"的效果
public class Child : Father, IFlyable, ISwimmable
{
// Father 是唯一的父类
// IFlyable、ISwimmable 是接口(可以多个)
}
8.2 规则二:万物皆 Object
C# 中,所有类最终都继承自 System.Object。
// 即使你不写继承,编译器也会自动加上
public class MyClass // 等价于 public class MyClass : Object
{
}
// Object 提供的基本方法:
// - ToString():返回对象的字符串表示
// - Equals():判断两个对象是否相等
// - GetHashCode():获取哈希码
// - GetType():获取对象的类型信息
8.3 规则三:sealed 阻止继承
如果某个类不想被继承,用 sealed 关键字:
public sealed class FinalClass
{
// 这个类不能被继承
}
// ❌ 编译错误!
// public class Child : FinalClass { }
8.4 规则四:访问修饰符决定了"继承什么"
public class Father
{
public string PublicField; // ✅ 子类可以访问
private string PrivateField; // ❌ 子类不能访问
protected string ProtectedField; // ✅ 子类可以访问
internal string InternalField; // ✅ 同项目内子类可访问
}
public class Son : Father
{
public void Test()
{
PublicField = "OK"; // ✅
ProtectedField = "OK"; // ✅ protected 就是给子类准备的
// PrivateField = "NO"; // ❌ 编译错误!private 不继承
InternalField = "OK"; // ✅(如果在同一个项目内)
}
}
| 修饰符 | 含义 | 子类能不能访问 |
|---|---|---|
public |
完全公开 | ✅ |
protected |
给子类用的 | ✅ |
private |
自己私有的 | ❌ |
internal |
同项目可见 | ✅(同项目) |
protected internal |
protected + internal | ✅ |
9. 继承 vs 组合 —— 何时用继承?
不是所有的"包含关系"都用继承! 这是新手最容易犯的错。
9.1 核心判断标准:is-a vs has-a
继承 = "是一个"(is-a) → Dog is a Animal(狗是一个动物)✅
组合 = "有一个"(has-a) → Car has a Engine(车有一个引擎)✅
9.2 反例:不该用继承的情况
// ❌ 错误设计:Car 继承 Engine(车是一个引擎?不对!)
public class Engine
{
public int HorsePower { get; set; }
public void Start() { }
}
public class Car : Engine // 语义错误!车不是引擎
{
// Car 被迫继承了所有 Engine 的东西
}
// ✅ 正确设计:Car 组合 Engine(车有一个引擎)
public class Car
{
public Engine Engine { get; set; } // 组合:车"有"一个引擎
public string Brand { get; set; }
public void Start()
{
Engine.Start(); // 委托给引擎
Console.WriteLine("汽车启动!");
}
}
9.3 错误案例:正方形继承长方形
这是一个经典的"继承陷阱":
// ❌ 数学上正方是特殊的长方,但代码中继承会出问题
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
}
public class Square : Rectangle
{
// 正方形要求宽=高,但父类允许分别设置
// 这就违反了"里氏替换原则"
public override int Width
{
set { base.Width = base.Height = value; }
}
}
9.4 选择指南
| 场景 | 用继承还是组合? | 原因 |
|---|---|---|
| 狗 → 动物 | ✅ 继承 | 狗是一个 动物 |
| 汽车 → 引擎 | ✅ 组合 | 汽车有一个 引擎 |
| 学生 → 人 | ✅ 继承 | 学生是一个 人 |
| 订单 → 商品列表 | ✅ 组合 | 订单有一个 商品列表 |
| 正方形 → 长方形 | ⚠️ 谨慎 | 可能导致行为不一致 |
10. 综合实战案例
让我们把学到的知识串起来,做一个完整的小项目。
场景:游戏角色系统
// ==================== 基础角色类(所有角色的父类)====================
public abstract class Character
{
// 所有角色都有的属性
public string Name { get; set; }
public int Level { get; set; } = 1;
public int Health { get; set; } = 100;
public int AttackPower { get; set; }
// 构造函数
public Character(string name, int attackPower)
{
Name = name;
AttackPower = attackPower;
}
// 所有角色都会的攻击(有默认实现)
public virtual void Attack(Character target)
{
Console.WriteLine($"{Name} 攻击了 {target.Name},造成 {AttackPower} 点伤害");
target.TakeDamage(AttackPower);
}
// 受伤逻辑(有默认实现)
public virtual void TakeDamage(int damage)
{
Health -= damage;
Console.WriteLine($"{Name} 受到 {damage} 点伤害,剩余生命:{Health}");
if (Health <= 0)
{
Console.WriteLine($"{Name} 被击败了!💀");
}
}
// 抽象方法 —— 子类必须实现的特殊技能
public abstract void UseSpecialSkill(Character target);
// 通用方法
public void ShowStatus()
{
Console.WriteLine($"[{Name}] 等级:{Level} | 生命:{Health} | 攻击力:{AttackPower}");
}
}
// ==================== 战士 ====================
public class Warrior : Character
{
public int Armor { get; set; } = 20; // 战士独有的护甲
public Warrior(string name) : base(name, 30)
{
}
// 战士的护甲减少伤害
public override void TakeDamage(int damage)
{
int reducedDamage = Math.Max(damage - Armor, 0);
Console.WriteLine($"{Name} 的护甲抵挡了 {damage - reducedDamage} 点伤害!");
base.TakeDamage(reducedDamage);
}
public override void UseSpecialSkill(Character target)
{
Console.WriteLine($"{Name} 使用【旋风斩】!");
int damage = AttackPower * 2;
Console.WriteLine($"造成 {damage} 点伤害!");
target.TakeDamage(damage);
}
}
// ==================== 法师 ====================
public class Mage : Character
{
public int Mana { get; set; } = 100; // 法师独有的法力值
public Mage(string name) : base(name, 40)
{
}
public override void UseSpecialSkill(Character target)
{
if (Mana >= 30)
{
Mana -= 30;
Console.WriteLine($"{Name} 使用【火球术】!法力值剩余:{Mana}");
int damage = AttackPower * 3;
Console.WriteLine($"造成 {damage} 点伤害!");
target.TakeDamage(damage);
}
else
{
Console.WriteLine($"{Name} 法力不足,无法释放技能!");
}
}
}
// ==================== 弓箭手 ====================
public class Archer : Character
{
public int CriticalChance { get; set; } = 30; // 暴击率
public Archer(string name) : base(name, 25)
{
}
public override void Attack(Character target)
{
Random rand = new Random();
if (rand.Next(100) < CriticalChance)
{
int critDamage = AttackPower * 2;
Console.WriteLine($"{Name} 暴击了!造成 {critDamage} 点伤害!💥");
target.TakeDamage(critDamage);
}
else
{
base.Attack(target);
}
}
public override void UseSpecialSkill(Character target)
{
Console.WriteLine($"{Name} 使用【连射】!");
base.Attack(target); // 射一箭
base.Attack(target); // 再射一箭
base.Attack(target); // 再射一箭(三次普通攻击)
}
}
// ==================== 战斗测试 ====================
class Program
{
static void Main(string[] args)
{
// 创建角色
Warrior warrior = new Warrior("亚瑟");
Mage mage = new Mage("甘道夫");
Archer archer = new Archer("莱戈拉斯");
// 显示初始状态
Console.WriteLine("========== 初始状态 ==========");
warrior.ShowStatus();
mage.ShowStatus();
archer.ShowStatus();
// 战斗开始
Console.WriteLine("\n========== 战斗开始!==========");
warrior.Attack(mage); // 战士普攻法师
mage.Attack(warrior); // 法师普攻战士(被护甲减少)
archer.Attack(mage); // 弓箭手攻击(可能暴击)
Console.WriteLine("\n========== 释放大招!==========");
warrior.UseSpecialSkill(mage); // 战士释放旋风斩
mage.UseSpecialSkill(warrior); // 法师释放火球术
archer.UseSpecialSkill(warrior);// 弓箭手释放连射
Console.WriteLine("\n========== 最终状态 ==========");
warrior.ShowStatus();
mage.ShowStatus();
archer.ShowStatus();
}
}
这个案例中继承的体现
| 继承的概念 | 在代码中的体现 |
|---|---|
| 基类定义共性 | Character 定义了 Name、Level、Health、Attack 等 |
| 子类添加特性 | Warrior 加了 Armor,Mage 加了 Mana |
| 重写方法 | Warrior.TakeDamage() 加入了护甲减伤 |
| 抽象方法强制实现 | 每个子类都必须实现 UseSpecialSkill() |
| base 调用父类逻辑 | Warrior.TakeDamage() 中调用了 base.TakeDamage() |
| 多态(统一处理) | 可以把所有角色放入 List<Character> 统一操作 |
11. 常见错误与自查清单
11.1 常见错误
错误一:忘记调用父类构造函数
public class Animal
{
public Animal(string name) { } // 没有无参构造函数!
}
public class Dog : Animal
{
// ❌ 编译错误!父类没有无参构造函数,子类又没有显式调用 base(name)
public Dog()
{
}
}
// ✅ 修复:
public class Dog : Animal
{
public Dog(string name) : base(name) { }
}
错误二:用 new 而不是 override
public class Animal
{
public virtual void MakeSound() { }
}
public class Dog : Animal
{
public new void MakeSound() { } // ⚠️ 隐藏了,不是重写!
}
// 后果:多态失效
Animal a = new Dog();
a.MakeSound(); // 调用的是 Animal 的版本,不是 Dog 的!
错误三:把不该继承的东西也继承了
public class Bird
{
public void Fly() { }
}
public class Penguin : Bird // 企鹅是鸟,但不会飞!
{
// 被迫继承了 Fly(),这很尴尬
}
11.2 自查清单
学完继承后,问自己这几个问题:
- 能不能说出
virtual和override的区别? - 能不能说出
override和new的区别? - 知道创建子类对象时,构造函数的执行顺序吗?
-
base关键字有哪些用法? -
private成员会被子类继承吗? - 什么时候用继承,什么时候用组合?
- C# 支持多继承吗?
- 所有类的最终祖先是谁?
12. 小结与记忆口诀
核心知识回顾
┌─────────────────────────────────────────────────────────────┐
│ 继 承 核 心 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 语法: class 子类 : 父类 { } │
│ │
│ 关键字: virtual(父类声明)+ override(子类重写) │
│ new(隐藏,不是重写,不推荐) │
│ base(调用父类构造函数或方法) │
│ sealed(阻止被继承) │
│ abstract(抽象,必须被继承和重写) │
│ │
│ 规则: • C# 单继承,多接口 │
│ • 构造函数先父后子 │
│ • private 成员不继承 │
│ • 所有类最终继承 Object │
│ │
│ 判断: "是一个" → 继承,"有一个" → 组合 │
│ │
└─────────────────────────────────────────────────────────────┘
记忆口诀
继承三大要点:
父类共性往上抽,
子类特性自己留。
virtual 声明 override 改,
base 调用父类不犯愁。构造顺序先有爹,
私有成员子不偷。
单继承来多接口,
is-a 判断记心头。
下一步学习建议:理解继承后,建议继续学习多态(Polymorphism) 和抽象类(Abstract Class),它们和继承紧密相连,共同构成了面向对象编程的三大支柱(封装、继承、多态)。