C#继承与多态全解析,让你的对象“活”起来

一、:继承(Inheritance)

1. 继承概念

继承(Inheritance)是一种机制,它允许一个类(子类/派生类)从另一个类(父类/基类)中继承属性和方法。 这种关系使得子类可以重用父类的代码,同时可以在子类中添加或修改属性和方法。 继承有助于减少代码重复和提高代码的可维护性。

2. C#中的继承语法

在C#中,实现继承非常简单,使用冒号:即可。

"is-a"关系:继承描述的是一种"is-a"(是一个)的关系。

第一步:创建基类 Enemy

csharp 复制代码
// 基类 / 父类
public class Enemy
{
    public int health;
    public int attackPower;

    public void Move()
    {
        Console.WriteLine("敌人正在移动。");
    }

    public void Attack()
    {
        Console.WriteLine("敌人发起了攻击!");
    }
}

第二步:创建派生类 GoblinSkeleton

csharp 复制代码
// 派生类 / 子类
public class Goblin : Enemy // Goblin 继承自 Enemy
{
    // 它自动拥有了 health, attackPower, Move(), Attack()
    public void StealGold()
    {
        Console.WriteLine("哥布林偷走了你的金币!");
    }
}

// 派生类 / 子类
public class Skeleton : Enemy // Skeleton 继承自 Enemy
{
    // 它也自动拥有了 health, attackPower, Move(), Attack()
    public void Reassemble()
    {
        Console.WriteLine("骷髅兵正在重组身体!");
    }
}

现在,代码变得干净、有条理了。我们可以这样使用它们:

csharp 复制代码
Goblin goblin = new Goblin();
goblin.health = 100;      // 继承自 Enemy
goblin.Move();            // 继承自 Enemy
goblin.StealGold();       // 自己的特有方法

Skeleton skeleton = new Skeleton();
skeleton.attackPower = 20; // 继承自 Enemy
skeleton.Attack();        // 继承自 Enemy
skeleton.Reassemble();    // 自己的特有方法

继承的好处

  • 代码重用:公共代码只需写一次。
  • 逻辑清晰:建立了清晰的类层次结构。
  • 易于维护:修改基类,所有子类都会自动更新。

3. 访问修饰符与继承

  • public:公开的。子类可以访问,外部代码也可以访问。
  • private:私有的。只有在当前类内部可以访问,子类无法访问
  • protected:受保护的。介于publicprivate之间。它允许在基类类和其所有子类中访问,但对外部代码是私有的。

让我们改进Enemy类:

csharp 复制代码
public class Enemy
{
    protected int health; // 改为 protected,子类可以直接访问,但外部不行
    private string internalId = "ENEMY_001"; // private,子类无法访问

    public int AttackPower { get; set; } // public 属性

    // ...
}

public class Goblin : Enemy
{
    public void TakeDamage(int damage)
    {
        this.health -= damage; // 正确!可以访问 protected 成员
        // this.internalId = "GOBLIN_ID"; // 错误!无法访问 private 成员
    }
}

4. 构造函数与 base 关键字

一个重要规则:子类不会继承父类的构造函数

当创建一个子类实例时,系统会先调用父类的构造函数,然后再调用子类的构造函数。这是为了确保在子类初始化之前,父类的部分已经被正确地创建了。

如果父类有一个带参数的构造函数,子类必须通过: base()语法显式地调用它。

csharp 复制代码
public class Enemy
{
    public int health;

    // 父类的构造函数
    public Enemy(int initialHealth)
    {
        this.health = initialHealth;
        Console.WriteLine("Enemy 构造函数被调用。");
    }
}

public class Goblin : Enemy
{
    public string weapon;

    // 子类的构造函数,通过 base 调用父类构造函数
    public Goblin(int initialHealth, string weapon) : base(initialHealth)
    {
        // 先调用 base(initialHealth),即父类的构造函数
        // 然后再执行这里的代码
        this.weapon = weapon;
        Console.WriteLine("Goblin 构造函数被调用。");
    }
}

// 使用
Goblin goblin = new Goblin(100, "生锈的匕首");
// 输出:
// Enemy 构造函数被调用。
// Goblin 构造函数被调用。

二、多态

1. 多态的概念

它允许不同类的对象对同一消息(方法调用)作出不同的响应

在C#中,多态主要通过虚方法( virtual )和方法重写( override )来实现,同时接口( interface )和抽象类( abstract class )也对多态提供了支持

2. 多态的实现

要在C#中实现多态,需要三个关键要素:

  1. virtual(虚拟) :在父类 的方法前加上virtual关键字,表示这个方法是"虚拟的",允许被子类重写(Override)。
  2. override(重写) :在子类 中,使用override关键字来提供一个父类virtual方法的新实现。
  3. 基类引用指向子类对象Enemy enemy = new Goblin();

让我们来升级我们的敌人系统,实现多态的Attack方法。

第一步:在 Enemy 中将 Attack 声明为 virtual

csharp 复制代码
public class Enemy
{
    // ... 其他成员 ...

    // virtual 关键字表示这个方法可以被子类重写
    public virtual void Attack()
    {
        Console.WriteLine("敌人发起了基础攻击!");
    }
}

第二步:在 GoblinSkeletonoverride 这个方法

csharp 复制代码
public class Goblin : Enemy
{
    // ...

    // override 关键字表示我们正在重写基类的同名方法
    public override void Attack()
    {
        // base.Attack(); // 可以选择性地调用基类的方法
        Console.WriteLine("哥布林用匕首进行了一次快速突刺!");
    }
}

public class Skeleton : Enemy
{
    // ...

    public override void Attack()
    {
        Console.WriteLine("骷髅兵挥舞着骨头棒砸向你!");
    }
}

第三步:见证多态的使用! 我们可以创建一个Enemy类型的列表,但里面可以装入各种不同类型的敌人!

csharp 复制代码
// 创建一个敌人列表
List<Enemy> enemies = new List<Enemy>();

// 添加不同类型的敌人,注意,都是用子类实例化的
enemies.Add(new Goblin());
enemies.Add(new Skeleton());
enemies.Add(new Enemy()); // 也可以添加一个普通的 Enemy

// 遍历敌人列表,让他们一起攻击
Console.WriteLine("====== 战斗开始 ======");
foreach (Enemy currentEnemy in enemies)
{
    // 最关键的一行代码!
    //实现多态,不同的对象对同一个方法做出不一样的响应。
    currentEnemy.Attack(); 
}

输出结果:

diff 复制代码
====== 战斗开始 ======
哥布林用匕首进行了一次快速突刺!
骷髅兵挥舞着骨头棒砸向你!
敌人发起了基础攻击!

三、深入探索 ------ abstract, sealednew

1. abstract 类与方法

  • 抽象类 (abstract class) :不能被实例化的类。new Enemy()会报错。它只能被用作基类。
  • 抽象方法 (abstract method) :在抽象类中,只有声明没有方法体的方法。它强制 所有非抽象的子类必须使用override来实现这个方法。
csharp 复制代码
// 抽象基类
public abstract class Enemy
{
    public int health;

    // 抽象方法,没有实现,用分号结尾
    // 它强制所有子类必须提供自己的攻击方式
    public abstract void Attack(); 

    // 抽象类中也可以有普通方法
    public void Move()
    {
        Console.WriteLine("敌人正在移动。");
    }
}

public class Goblin : Enemy
{
    // 必须重写父类的抽象方法 Attack
    public override void Attack()
    {
        Console.WriteLine("哥布林用匕首进行了一次快速突刺!");
    }
}

abstract vs virtual 的区别:

  • virtual 方法 实现体,子类可以重写或者不重写都行。
  • abstract 方法没有 实现体,子类必须 重写它(除非子类自己也是abstract的)。
  • abstract 方法只能存在于abstract 类中。

2. sealed 类与方法继承的终点

abstract相反,sealed关键字用来阻止继承

  • 密封类 (sealed class):不能被任何其他类继承。
  • 密封方法 (sealed override) :在一个子类中,可以把一个重写的方法标记为sealed,这样它的下一级子类就不能再继续重写这个方法了。
csharp 复制代码
// 密封类,不能被继承
public sealed class SuperBoss : Enemy
{
    public override void Attack()
    {
        Console.WriteLine("超级Boss发动了灭世攻击!");
    }
}

// public class MiniBoss : SuperBoss { }  // 错误!无法从密封类型"SuperBoss"派生

public class SkeletonWarrior : Skeleton
{
    // 将 Attack 方法密封,SkeletonWarrior 的子类将无法再重写它
    public sealed override void Attack()
    {
        Console.WriteLine("骷髅战士发动了精准的剑击!");
    }
}

3. new 关键字

当子类中一个方法的签名(名称和参数)与父类完全相同时,你可以用new关键字来"隐藏"父类的方法。

这不是多态!

csharp 复制代码
public class Parent
{
    public void Greet()
    {
        Console.WriteLine("Hello from Parent!");
    }
}

public class Child : Parent
{
    // 使用 new 关键字隐藏了父类的 Greet 方法
    public new void Greet()
    {
        Console.WriteLine("Hello from Child!");
    }
}

// 观察区别
Parent p1 = new Parent();
p1.Greet(); // 输出: Hello from Parent!

Child c1 = new Child();
c1.Greet(); // 输出: Hello from Child!

// 关键区别在这里!
Parent p2 = new Child(); // 父类引用指向子类对象
p2.Greet(); // 输出: Hello from Parent!

总结:override vs new

  • override 是真正的多态。运行时决定调用哪个版本。
  • new 是方法隐藏。编译时根据引用的类型决定调用哪个版本。
  • 绝大多数情况下,你想要的是override而不是new

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

相关推荐
狗哥哥1 小时前
Swagger对接MCP服务:赋能AI编码的高效落地指南
前端·后端
zl_vslam1 小时前
SLAM中的非线性优-3D图优化之相对位姿Between Factor(六)
前端·人工智能·算法·计算机视觉·slam se2 非线性优化
申阳1 小时前
Day 18:01. 基于 SpringBoot4 开发后台管理系统-快速了解一下 SpringBoot4 新特性
前端·后端·程序员
500佰1 小时前
技术包办模式给我带来的反思
前端
g***72701 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
用户41429296072391 小时前
解决「买不到、买得贵、买得慢」:反向海淘独立站的核心功能设计与案例复盘
前端·后端·架构
N***p3651 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
五号厂房1 小时前
网络请求库通用封装(GET/POST + 超时 + 传参)+fetch
前端
小雨青年1 小时前
智能交互新范式:拒绝“黑盒”,带你用 MateChat 与 DSL 构建“高可靠”的 NL2UI 引擎
前端·ai·华为云