目 录CONTENT

文章目录

CSharp(二十四)类(Class)继承

CSharp(二十四)类(Class)继承


目录

  1. 什么是继承?—— 生活例子秒懂
  2. 继承的基本语法
  3. 用动物例子深入理解
  4. 构造函数在继承中的执行顺序
  5. virtual 和 override —— 方法重写
  6. base 关键字详解
  7. 多层继承:爷爷 → 爸爸 → 儿子
  8. C# 继承的重要规则
  9. 继承 vs 组合 —— 何时用继承?
  10. 综合实战案例
  11. 常见错误与自查清单
  12. 小结与记忆口诀

1. 什么是继承?—— 生活例子秒懂

1.1 生活中的继承

先忘掉代码,想想现实生活:

你的父母有房子、车子,你去世后这些财产会传给你——这就是"继承"。

在编程里,继承也是类似的意思:

  • 父类(基类):把"财产"(字段、属性、方法)准备好
  • 子类(派生类):自动拥有这些"财产",不用重新写一遍
  • 子类还可以添加自己的新东西,甚至改造父类留下的东西

1.2 一个直观比喻

想象你在开发一个宠物店管理系统:

动物(Animal)
├── 狗(Dog)
├── 猫(Cat)
└── 鸟(Bird)

所有动物都有:名字、年龄、吃东西。这些共性写在 Animal 里。

狗有自己的特点:品种、汪汪叫、捡球。这些写在 Dog 里,同时自动拥有 Animal 的所有内容。

这就是继承:把共同的东西抽到父类,子类只管自己的特色。

1.3 继承解决了什么问题?

没有继承(各写各的) 有继承(统一管理)
Dog 类里写 Name、Age、Eat 写在 Animal 里一次
Cat 类里又写一遍 Name、Age、Eat DogCatBird 全部自动拥有
修改 "吃东西" 逻辑要改 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 里),但 DogCatBird 都能用
  • MakeSound() 每个子类各写各的,体现出不同动物的特性
  • 每种动物还加了自己的独门绝技(FetchBallClimbTreeFly

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 自查清单

学完继承后,问自己这几个问题:

  • 能不能说出 virtualoverride 的区别?
  • 能不能说出 overridenew 的区别?
  • 知道创建子类对象时,构造函数的执行顺序吗?
  • base 关键字有哪些用法?
  • private 成员会被子类继承吗?
  • 什么时候用继承,什么时候用组合?
  • C# 支持多继承吗?
  • 所有类的最终祖先是谁?

12. 小结与记忆口诀

核心知识回顾

┌─────────────────────────────────────────────────────────────┐
│                        继 承 核 心                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  语法:    class 子类 : 父类 { }                             │
│                                                             │
│  关键字:  virtual(父类声明)+ override(子类重写)           │
│           new(隐藏,不是重写,不推荐)                       │
│           base(调用父类构造函数或方法)                      │
│           sealed(阻止被继承)                                │
│           abstract(抽象,必须被继承和重写)                  │
│                                                             │
│  规则:    • C# 单继承,多接口                               │
│           • 构造函数先父后子                                 │
│           • private 成员不继承                               │
│           • 所有类最终继承 Object                            │
│                                                             │
│  判断:    "是一个" → 继承,"有一个" → 组合                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

记忆口诀

继承三大要点

父类共性往上抽,
子类特性自己留。
virtual 声明 override 改,
base 调用父类不犯愁。

构造顺序先有爹,
私有成员子不偷。
单继承来多接口,
is-a 判断记心头。


下一步学习建议:理解继承后,建议继续学习多态(Polymorphism)抽象类(Abstract Class),它们和继承紧密相连,共同构成了面向对象编程的三大支柱(封装、继承、多态)。

0
博主关闭了当前页面的评论