目 录CONTENT

文章目录

CSharp(二十六)类(Class)抽象类与抽象方法

CSharp(二十六)类(Class)抽象类与抽象方法

一、从一个真实的问题开始

假设你正在开发一个游戏,里面有战士法师弓箭手三种职业。它们都有一个共同的行为——Attack(攻击),但每种职业的攻击方式不同:

  • 战士:拿剑砍
  • 法师:放火球
  • 弓箭手:射箭

如果你写三个独立的类,代码会变成这样:

public class Warrior
{
    public void Attack() { Console.WriteLine("战士挥剑砍击!"); }
}

public class Mage
{
    public void Attack() { Console.WriteLine("法师释放火球术!"); }
}

public class Archer
{
    public void Attack() { Console.WriteLine("弓箭手射出致命一箭!"); }
}

问题来了——如何统一管理这三种职业?比如你想把所有角色放进一个队伍里,然后让所有角色一起攻击,该怎么办?

这时候就需要抽象类登场了。


二、什么是抽象类?(用生活例子理解)

一句话定义:抽象类是一个"不完整的模板",它告诉你必须做什么,但不告诉你具体怎么做

生活类比 1:电器说明书

你买了一台"抽象电器",说明书上写着:

  • ✅ 必须有开关按钮(这是必须做的事
  • ❓ 但怎么开关,每个厂家的电器不一样(这是具体怎么做,留给子类实现)

生活类比 2:餐厅菜单

一张菜单写着"主食 + 汤 + 小菜"(这是模板),但具体是什么主食、什么汤,由每家分店自己决定。

生活类比 3:学校考试大纲

考试大纲规定:必须考语文、数学、英语(抽象方法),但每张试卷的具体题目不同(子类实现)。

核心思想:抽象类 = 制定规则 + 提供通用工具,把"个性化"的部分留给子类自由发挥。


三、抽象类的基本语法

3.1 声明抽象类

public abstract class 类名
{
    // 可以有的东西 ↓
}

abstract 关键字修饰类即可。

3.2 抽象类的内部可以有什么?

成员类型 可以有吗? 说明
字段(Field) ✅ 可以 普通变量,存储数据
属性(Property) ✅ 可以 普通属性 + 抽象属性
方法(Method) ✅ 可以 普通方法 + 抽象方法 + 虚方法
构造函数 ✅ 可以 但不能直接 new 这个类
事件(Event) ✅ 可以
索引器 ✅ 可以

3.3 最重要的限制:不能 new

public abstract class Animal { }

// ❌ 编译错误!抽象类不能实例化
Animal a = new Animal();

// ✅ 正确:通过继承的子类来创建对象
public class Dog : Animal { }
Animal a = new Dog();  // 这样可以

为什么要这样设计? 因为抽象类本身就是"不完整"的——它里面的抽象方法没有代码。如果允许直接 new,调用抽象方法时程序就不知道该执行什么了。


四、抽象方法(Abstract Method)

4.1 什么是抽象方法?

抽象方法是一个只有签名、没有方法体的方法。它用分号 ; 直接结束,后面没有 { }

public abstract class Animal
{
    // 抽象方法:只声明,不实现
    public abstract void MakeSound();

    // 普通方法:有完整实现
    public void Sleep()
    {
        Console.WriteLine("Zzz... 动物在睡觉");
    }
}

4.2 子类必须重写抽象方法

public class Dog : Animal
{
    // 必须用 override 重写抽象方法
    public override void MakeSound()
    {
        Console.WriteLine("汪汪汪!");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("喵喵喵!");
    }
}

如果不重写会怎样? 编译器直接报错!

错误 CS0534: 'Dog' 未实现继承的抽象成员 'Animal.MakeSound()'

这是抽象类最大的优势——编译器强制子类实现规定的方法,防止你忘记。

4.3 完整示例:动物叫声系统

using System;
using System.Collections.Generic;

public abstract class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    // 抽象方法:每个动物必须实现
    public abstract void MakeSound();

    // 普通方法:所有动物通用
    public void Eat()
    {
        Console.WriteLine($"{Name} 正在吃东西...");
    }
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 说:汪汪汪!");
    }
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 说:喵喵喵!");
    }
}

public class Duck : Animal
{
    public Duck(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 说:嘎嘎嘎!");
    }
}

// ========== 使用 ==========
class Program
{
    static void Main()
    {
        // 用抽象类类型做集合,实现多态
        List<Animal> zoo = new List<Animal>
        {
            new Dog("旺财"),
            new Cat("咪咪"),
            new Duck("唐老鸭")
        };

        foreach (var animal in zoo)
        {
            animal.MakeSound();   // 每个动物发出不同声音
            animal.Eat();         // 通用行为
        }
    }
}

/* 输出:
旺财 说:汪汪汪!
旺财 正在吃东西...
咪咪 说:喵喵喵!
咪咪 正在吃东西...
唐老鸭 说:嘎嘎嘎!
唐老鸭 正在吃东西...
*/

五、抽象属性(Abstract Property)

属性也可以被标记为抽象的。同样,只有声明,没有实现。

public abstract class Shape
{
    // 抽象属性:子类必须实现
    public abstract double Area { get; }
    public abstract string Color { get; set; }

    // 普通属性(可被子类继承使用)
    public DateTime CreatedAt { get; } = DateTime.Now;
}

public class Circle : Shape
{
    private double _radius;
    private string _color;

    public Circle(double radius)
    {
        _radius = radius;
    }

    // 实现抽象属性
    public override double Area => Math.PI * _radius * _radius;

    public override string Color
    {
        get => _color;
        set => _color = value;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    private string _color;

    public override double Area => Width * Height;

    public override string Color
    {
        get => _color;
        set => _color = value;
    }
}

六、抽象类中的构造函数

很多人误解:"抽象类不能 new,所以它没有构造函数?"

错! 抽象类可以有构造函数,但只有子类实例化时才会调用。

public abstract class Person
{
    public string Name { get; }
    public int Age { get; }

    // 抽象类的构造函数
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
        Console.WriteLine($"抽象类构造函数被调用:{Name}, {Age}岁");
    }

    public abstract void Work();
}

public class Teacher : Person
{
    public string Subject { get; }

    // 子类构造函数中通过 base 调用抽象类构造函数
    public Teacher(string name, int age, string subject)
        : base(name, age)   // ← 这里调用抽象类构造函数
    {
        Subject = subject;
        Console.WriteLine("教师构造函数被调用");
    }

    public override void Work()
    {
        Console.WriteLine($"{Name} 老师正在教 {Subject}");
    }
}

// 使用
class Program
{
    static void Main()
    {
        Teacher t = new Teacher("张老师", 35, "数学");
        t.Work();
    }
}

/* 输出:
抽象类构造函数被调用:张老师, 35岁
教师构造函数被调用
张老师 老师正在教 数学
*/

理解:创建子类对象时,先执行抽象类的构造函数,再执行子类的构造函数。就像盖房子先打地基(抽象类),再盖楼上的房间(子类)。


七、抽象方法 vs 虚方法(Virtual Method)

这两个概念容易混淆,这里一次性彻底讲清楚:

对比维度 abstract 抽象方法 virtual 虚方法
父类中必须有实现? 不能有方法体,只有声明 必须有默认实现
子类是否必须重写? 必须重写,否则编译报错 ❌ 可选,不重写就用父类默认实现
用哪个关键字重写? override override
设计意图 "你必须自己决定怎么做" "我给你一个默认方案,你可以改"
类比 考试必答题 附加题(可做可不做)

代码对比

public abstract class Bird
{
    // 抽象方法:每种鸟必须自己实现
    public abstract void Fly();

    // 虚方法:有默认实现,子类可以不重写
    public virtual void Sing()
    {
        Console.WriteLine("叽叽喳喳...");
    }
}

public class Eagle : Bird
{
    // 必须实现抽象方法
    public override void Fly()
    {
        Console.WriteLine("老鹰展翅高飞!");
    }

    // 可选:重写虚方法
    public override void Sing()
    {
        Console.WriteLine("唳——!");
    }
}

public class Penguin : Bird
{
    // 必须实现抽象方法
    public override void Fly()
    {
        Console.WriteLine("企鹅不会飞,但会游泳~");
    }

    // 不重写 Sing(),使用默认的"叽叽喳喳..."
}

选择建议

  • 如果必须让子类自己决定怎么做 → 用 abstract
  • 如果提供一个默认方案,子类爱改不改 → 用 virtual

八、回到最初的问题:用抽象类重构游戏角色

让我们用抽象类优雅地解决开头的游戏角色问题:

using System;
using System.Collections.Generic;

// ========== 抽象角色基类 ==========
public abstract class Character
{
    public string Name { get; set; }
    public int HP { get; set; }

    public Character(string name, int hp)
    {
        Name = name;
        HP = hp;
    }

    // 抽象方法:每个职业有不同的攻击方式
    public abstract void Attack();

    // 抽象方法:不同职业有不同技能
    public abstract void UseSkill();

    // 普通方法:所有角色通用的受伤逻辑
    public void TakeDamage(int damage)
    {
        HP -= damage;
        Console.WriteLine($"{Name} 受到 {damage} 点伤害,剩余血量:{HP}");
        if (HP <= 0)
            Console.WriteLine($"{Name} 已阵亡!");
    }
}

// ========== 战士 ==========
public class Warrior : Character
{
    public Warrior(string name) : base(name, 150) { }

    public override void Attack()
    {
        Console.WriteLine($"{Name}(战士)挥动长剑,造成 30 点伤害!");
    }

    public override void UseSkill()
    {
        Console.WriteLine($"{Name}(战士)释放【旋风斩】!");
    }
}

// ========== 法师 ==========
public class Mage : Character
{
    public Mage(string name) : base(name, 80) { }

    public override void Attack()
    {
        Console.WriteLine($"{Name}(法师)释放火球术,造成 45 点伤害!");
    }

    public override void UseSkill()
    {
        Console.WriteLine($"{Name}(法师)释放【暴风雪】!");
    }
}

// ========== 弓箭手 ==========
public class Archer : Character
{
    public Archer(string name) : base(name, 100) { }

    public override void Attack()
    {
        Console.WriteLine($"{Name}(弓箭手)射出箭矢,造成 35 点伤害!");
    }

    public override void UseSkill()
    {
        Console.WriteLine($"{Name}(弓箭手)释放【箭雨】!");
    }
}

// ========== 游戏运行 ==========
class Program
{
    static void Main()
    {
        // 用抽象类类型统一管理所有角色
        List<Character> team = new List<Character>
        {
            new Warrior("亚瑟"),
            new Mage("甘道夫"),
            new Archer("莱戈拉斯")
        };

        Console.WriteLine("=== 第一回合:全体攻击 ===");
        foreach (var c in team)
            c.Attack();

        Console.WriteLine("\n=== 第二回合:全体释放技能 ===");
        foreach (var c in team)
            c.UseSkill();

        Console.WriteLine("\n=== 敌人反击 ===");
        foreach (var c in team)
            c.TakeDamage(40);
    }
}

运行结果

=== 第一回合:全体攻击 ===
亚瑟(战士)挥动长剑,造成 30 点伤害!
甘道夫(法师)释放火球术,造成 45 点伤害!
莱戈拉斯(弓箭手)射出箭矢,造成 35 点伤害!

=== 第二回合:全体释放技能 ===
亚瑟(战士)释放【旋风斩】!
甘道夫(法师)释放【暴风雪】!
莱戈拉斯(弓箭手)释放【箭雨】!

=== 敌人反击 ===
亚瑟 受到 40 点伤害,剩余血量:110
甘道夫 受到 40 点伤害,剩余血量:40
莱戈拉斯 受到 40 点伤害,剩余血量:60

关键价值:现在要新增一个"牧师"职业,你只需要写一个新类继承 Character,不用改动任何现有代码。这就是开闭原则——对扩展开放,对修改关闭。


九、抽象类 vs 接口 — 终极对比

这是面试必考、工作必用的知识点。

对比维度 抽象类 (abstract class) 接口 (interface)
关键字 abstract class interface
能有字段吗 ✅ 可以有 ❌ 不能(只能属性、方法、事件、索引器)
能有构造函数吗 ✅ 可以有 ❌ 不能
方法有实现吗 ✅ 可以有(普通方法 / 虚方法) ❌(C# 8.0 之前)/ ✅(C# 8.0+ 支持默认实现)
继承数量 只能继承 1 个抽象类 可以实现多个接口
访问修饰符 成员可以有任何修饰符 成员默认 public,不能改
设计关系 "是什么"(is-a) "能做什么"(can-do)
使用场景 有共同代码要复用,有共同特征 定义某种能力或契约

什么时候用抽象类?什么时候用接口?

用抽象类的场景:

  • 多个类之间有共同代码可以复用(比如公共字段、公共方法逻辑)
  • 类之间有天然的父子关系(狗是动物,汽车是交通工具)
  • 需要构造函数来初始化公共数据

用接口的场景:

  • 只想定义能力(能飞的、能游泳的、能比较大小的)
  • 需要多继承(一个类可以有多个接口)
  • 不同体系的类需要共享同一种行为

混合使用示例

// 抽象类:提供公共代码
public abstract class Animal
{
    public string Name { get; set; }
    public abstract void Eat();
    public void Sleep() => Console.WriteLine($"{Name} 在睡觉...");
}

// 接口:定义额外能力
public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

// 子类:继承抽象类 + 实现多个接口
public class Duck : Animal, IFlyable, ISwimmable
{
    public override void Eat()
    {
        Console.WriteLine($"{Name} 在吃小鱼");
    }

    public void Fly()
    {
        Console.WriteLine($"{Name} 飞起来了");
    }

    public void Swim()
    {
        Console.WriteLine($"{Name} 在水里游");
    }
}

// 使用
Duck donald = new Duck { Name = "唐老鸭" };
donald.Eat();    // 来自抽象类
donald.Sleep();  // 来自抽象类(继承的通用方法)
donald.Fly();    // 来自接口 IFlyable
donald.Swim();   // 来自接口 ISwimmable

十、常见错误与注意事项

错误 1:尝试实例化抽象类

public abstract class Base { }
Base b = new Base();  // ❌ 编译错误 CS0144

错误 2:忘记 override 关键字

public abstract class Base
{
    public abstract void DoSomething();
}

public class Derived : Base
{
    // ❌ 编译错误!缺少 override
    public void DoSomething() { }

    // ✅ 正确写法
    // public override void DoSomething() { }
}

错误 3:抽象方法不能是 private

public abstract class Base
{
    // ❌ 编译错误!抽象方法不能是 private
    // private abstract void DoSomething();
}

抽象方法必须被子类重写,private 意味着子类根本看不到它,所以矛盾了。

错误 4:抽象方法不能是 static

public abstract class Base
{
    // ❌ 编译错误!抽象方法不能是 static
    // public static abstract void DoSomething();
}

static 方法属于类本身,不参与继承,而抽象方法就是为了被继承重写的。

错误 5:sealedabstract 不能共存

// ❌ 编译错误!sealed 阻止继承,abstract 要求继承,矛盾
// public sealed abstract class Impossible { }

错误 6:抽象类中 virtualabstract 的区别没搞清楚

public abstract class Base
{
    // ✅ abstract: 子类必须实现
    public abstract void MustDo();

    // ✅ virtual: 有默认实现,子类可选重写
    public virtual void OptionalDo()
    {
        Console.WriteLine("默认实现");
    }
}

十一、课堂练习

练习 1:形状计算器

创建一个抽象类 Shape,包含:

  • 抽象属性 Area(只读,计算面积)
  • 抽象属性 Perimeter(只读,计算周长)
  • 抽象方法 Describe()(输出形状描述)

然后创建 Circle(圆)和 Triangle(三角形)子类。

点击查看参考答案
public abstract class Shape
{
    public abstract double Area { get; }
    public abstract double Perimeter { get; }
    public abstract void Describe();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(double radius) { Radius = radius; }

    public override double Area => Math.PI * Radius * Radius;
    public override double Perimeter => 2 * Math.PI * Radius;

    public override void Describe()
    {
        Console.WriteLine($"这是一个半径为 {Radius} 的圆,面积:{Area:F2},周长:{Perimeter:F2}");
    }
}

public class Triangle : Shape
{
    public double A { get; set; }
    public double B { get; set; }
    public double C { get; set; }

    public Triangle(double a, double b, double c)
    {
        A = a; B = b; C = c;
    }

    public override double Area
    {
        get
        {
            double s = (A + B + C) / 2;
            return Math.Sqrt(s * (s - A) * (s - B) * (s - C));
        }
    }

    public override double Perimeter => A + B + C;

    public override void Describe()
    {
        Console.WriteLine($"这是一个边长分别为 {A}、{B}、{C} 的三角形,面积:{Area:F2},周长:{Perimeter:F2}");
    }
}

练习 2:支付系统

创建一个抽象类 PaymentMethod,包含:

  • 抽象方法 Pay(decimal amount) 执行支付
  • 抽象方法 Refund(decimal amount) 执行退款
  • 字段 Balance(余额)

创建 Alipay(支付宝)和 WeChatPay(微信支付)子类。

点击查看参考答案
public abstract class PaymentMethod
{
    public string AccountName { get; set; }
    protected decimal Balance;

    public PaymentMethod(string accountName, decimal initialBalance)
    {
        AccountName = accountName;
        Balance = initialBalance;
    }

    public abstract void Pay(decimal amount);
    public abstract void Refund(decimal amount);

    public void ShowBalance()
    {
        Console.WriteLine($"{AccountName} 当前余额:{Balance:C}");
    }
}

public class Alipay : PaymentMethod
{
    public Alipay(string account, decimal balance) : base(account, balance) { }

    public override void Pay(decimal amount)
    {
        if (Balance >= amount)
        {
            Balance -= amount;
            Console.WriteLine($"[支付宝] {AccountName} 支付 {amount:C} 成功");
        }
        else
            Console.WriteLine($"[支付宝] {AccountName} 余额不足!");
    }

    public override void Refund(decimal amount)
    {
        Balance += amount;
        Console.WriteLine($"[支付宝] {AccountName} 退款 {amount:C} 到账");
    }
}

public class WeChatPay : PaymentMethod
{
    public WeChatPay(string account, decimal balance) : base(account, balance) { }

    public override void Pay(decimal amount)
    {
        if (Balance >= amount)
        {
            Balance -= amount;
            Console.WriteLine($"[微信支付] {AccountName} 支付 {amount:C} 成功");
        }
        else
            Console.WriteLine($"[微信支付] {AccountName} 余额不足!");
    }

    public override void Refund(decimal amount)
    {
        Balance += amount;
        Console.WriteLine($"[微信支付] {AccountName} 退款 {amount:C} 到账");
    }
}

十二、知识总结

一张图记住抽象类

┌─────────────────────────────────────────────┐
│           abstract class (抽象类)           │
│                                              │
│   ┌─────────────────────────────────────┐   │
│   │  abstract 成员(必须被子类实现)      │   │
│   │  · 抽象方法                          │   │
│   │  · 抽象属性                          │   │
│   └─────────────────────────────────────┘   │
│                                              │
│   ┌─────────────────────────────────────┐   │
│   │  普通成员(所有子类共享/可选重写)    │   │
│   │  · 字段                              │   │
│   │  · 普通方法                          │   │
│   │  · 虚方法(virtual)                 │   │
│   │  · 构造函数                          │   │
│   │  · 属性(非抽象)                    │   │
│   └─────────────────────────────────────┘   │
│                                              │
│   不能 new,只能通过子类实例化               │
└─────────────────────────────────────────────┘

三句话记住核心要点

  1. 抽象类 = 模板:规定"必须做什么"(抽象方法) + 提供"公共工具"(普通方法)
  2. 不能 new:抽象类不完整,必须通过子类来创建对象
  3. override 是桥梁:子类用 override 实现抽象方法,编译器帮你检查是否遗漏

面试速记口诀

"抽象不实例,方法无身体,子类必须覆,编译来帮你"

  • 象不例 → 不能 new
  • 体 → 抽象方法没有 { },只有 ;
  • 覆 → 子类必须 override 所有抽象成员
  • 你 → 如果遗漏,编译器直接报错提醒

下一步学习建议:掌握了抽象类之后,建议继续学习 接口(Interface)多态(Polymorphism),它们是面向对象编程中与抽象类紧密配合的核心概念。

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