目 录CONTENT

文章目录

CSharp(二十五)类(Class)多态

CSharp(二十五)类(Class)多态

目录

  1. 什么是多态 —— 从小明的遥控器说起
  2. 为什么需要多态 —— 没有多态的世界有多痛苦
  3. 多态的底层机制 —— 三要素缺一不可
  4. virtual 关键字详解
  5. override 关键字详解
  6. base 关键字 —— 在子类中调用父类的方法
  7. 方法隐藏(new 关键字)—— 这不是多态!
  8. virtual vs new —— 一张表彻底搞清
  9. 抽象类与多态
  10. 接口与多态
  11. 实战案例:游戏角色系统
  12. 实战案例:工资计算系统
  13. 常见错误与避坑指南
  14. 最佳实践
  15. 课后练习
  16. 总结

1. 什么是多态 —— 从小明的遥控器说起

一个生活中的例子

小明家里有一个万能遥控器,这个遥控器上只有一个"开关"按钮:

  • 对着电视按"开关" → 电视打开/关闭
  • 对着空调按"开关" → 空调启动/停止
  • 对着电风扇按"开关" → 风扇开始转/停止

同一个按钮(同一个方法名),作用在不同的电器上(不同的对象),产生不同的效果(不同的行为)—— 这就是多态。

编程中的多态

在程序里,"多态"指的是:

用父类类型的变量,调用子类重写后的方法,实际执行的是子类的版本。

用人话来说就是:你拿着一张"员工卡"(父类引用),刷不同的员工(子类对象),每个人干的事不一样。

// 父类:电器
public class Appliance
{
    public virtual void PressSwitch()
    {
        Console.WriteLine("电器:开关已按下(默认行为)");
    }
}

// 子类:电视
public class TV : Appliance
{
    public override void PressSwitch()
    {
        Console.WriteLine("电视:屏幕亮了!");
    }
}

// 子类:空调
public class AirConditioner : Appliance
{
    public override void PressSwitch()
    {
        Console.WriteLine("空调:开始制冷,呼呼呼~");
    }
}

// 子类:风扇
public class Fan : Appliance
{
    public override void PressSwitch()
    {
        Console.WriteLine("风扇:扇叶转起来了!");
    }
}

// 使用
Appliance[] devices = { new TV(), new AirConditioner(), new Fan() };
foreach (var device in devices)
{
    device.PressSwitch();  // 同一个方法调用,不同的实际行为!
}

/* 输出:
   电视:屏幕亮了!
   空调:开始制冷,呼呼呼~
   风扇:扇叶转起来了!
*/

关键点:变量 device 的类型是 Appliance(父类),但实际调用的方法取决于它指向的是哪个子类对象。这就是"多态"的核心。


2. 为什么需要多态 —— 没有多态的世界有多痛苦

场景:计算不同形状的面积

假设我们要写一个程序,计算圆形、矩形、三角形的面积。

没有多态的写法(痛苦版)

// 定义一个圆
Circle c = new Circle(5);
double circleArea = c.GetCircleArea();

// 定义一个矩形
Rectangle r = new Rectangle(4, 6);
double rectArea = r.GetRectangleArea();

// 定义一个三角形
Triangle t = new Triangle(3, 8);
double triArea = t.GetTriangleArea();

// 汇总面积
double total = circleArea + rectArea + triArea;

问题来了

  • 每新增一种形状,就要新增一种变量、一种计算方法、手动加一行汇总
  • 如果形状有100种呢?代码会爆炸
  • 无法统一处理,没法用循环,没法用集合

有多态的写法(快乐版)

// 所有形状都继承自 Shape,统一用 Shape 类型的列表管理
List<Shape> shapes = new List<Shape>
{
    new Circle(5),
    new Rectangle(4, 6),
    new Triangle(3, 8)
};

// 不管有多少种形状,同一个循环搞定
double total = 0;
foreach (Shape s in shapes)
{
    total += s.GetArea();  // 多态:自动调用对应子类的方法
}

// 如果新增一种形状,只需要:
// 1. 建一个新类继承 Shape
// 2. 重写 GetArea()
// 3. new 一个对象丢进列表
// 主逻辑代码一行都不用改!

多态的核心价值

  1. 可扩展性:新增类型不影响已有代码(开闭原则)
  2. 统一处理:可以用一个循环处理不同类型
  3. 代码简洁:消除大量 if-else 和类型判断

3. 多态的底层机制 —— 三要素缺一不可

多态不是魔术,它需要三个条件同时满足:

要素 说明 在哪里写
① 基类定义 virtual 方法 告诉C#:这个方法可以被子类重写 父类中
② 派生类使用 override 重写 告诉C#:我要用自己的版本来替换父类的 子类中
③ 通过基类引用调用 变量声明为父类类型,实际指向子类对象 调用处
// ① 基类中:标记为 virtual
public class Animal
{
    public virtual void Speak()       // ← virtual:允许子类重写
    {
        Console.WriteLine("动物在叫...");
    }
}

// ② 派生类中:标记为 override
public class Dog : Animal
{
    public override void Speak()      // ← override:实际重写
    {
        Console.WriteLine("汪汪汪!");
    }
}

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

// ③ 使用基类引用调用
Animal myPet;          // 变量类型是 Animal(基类)

myPet = new Dog();     // 实际指向 Dog 对象
myPet.Speak();         // 输出:汪汪汪!  (调用 Dog 的 Speak)

myPet = new Cat();     // 实际指向 Cat 对象
myPet.Speak();         // 输出:喵喵喵!  (调用 Cat 的 Speak)

记忆口诀virtual 说"你可以改",override 说"我真的改了",基类引用说"不管你是什么,用你的方式做"。


4. virtual 关键字详解

什么是 virtual?

virtual 是放在父类方法前的关键字,意思是:"这个方法我提供了默认实现,但子类可以自己重新写一份。"

public class 动物
{
    // 普通方法:子类不能重写
    public void 呼吸()
    {
        Console.WriteLine("所有动物都要呼吸");
    }

    // 虚方法:子类可以重写
    public virtual void 叫()
    {
        Console.WriteLine("动物发出了声音");
    }
}

virtual 的特点

特性 说明
必须有方法体 virtual 方法不能是空的,必须提供默认实现
不强制重写 子类可以不重写,那就用父类的默认版本
可以被链式重写 子类重写后,孙子类还可以继续重写
不能是 private 私有方法没有多态的意义
不能和 static 一起用 静态方法和实例无关,无法多态

virtual 属性的用法

不仅是方法,属性也可以标记为 virtual

public class Employee
{
    public virtual double Salary { get; set; } = 5000;

    public virtual double CalculateBonus()
    {
        return Salary * 0.1;  // 默认奖金:10%
    }
}

public class Manager : Employee
{
    public override double CalculateBonus()
    {
        return Salary * 0.3;  // 经理奖金:30%
    }
}

5. override 关键字详解

什么是 override?

override 是放在子类方法前的关键字,意思是:"父类的方法不够好,我用自己的版本替换它。"

public class 狗 : 动物
{
    public override void 叫()     // override:替换父类的"叫"
    {
        Console.WriteLine("汪汪汪!");
    }
}

override 的规则(重要!)

规则 说明 错误示例
只能重写 virtual/abstract/override 方法 父类没标记这三个关键字就不能重写 父类 public void A(){} → 子类不能 override
签名必须完全一致 方法名、参数类型、返回值必须一样 父类 int Get() → 子类 string Get()
访问级别不能更严格 父类 public,子类不能改成 protected 父类 public virtual → 子类 protected override
可以用 sealed 阻止继续重写 sealed override 表示到此为止 见下方示例

sealed override —— 到此为止

如果你不希望孙子类再重写这个方法,加 sealed

public class Animal
{
    public virtual void Speak() { }
}

public class Dog : Animal
{
    public sealed override void Speak()    // 封死了
    {
        Console.WriteLine("汪汪!");
    }
}

public class Husky : Dog
{
    // public override void Speak() { }   // ❌ 编译错误!Dog 已经 sealed 了
}

6. base 关键字 —— 在子类中调用父类的方法

有时候,子类的逻辑是"在父类的基础上加点东西",而不是完全替换。这时候用 base

public class Logger
{
    public virtual void Log(string message)
    {
        Console.WriteLine($"[{DateTime.Now}] {message}");
    }
}

public class FileLogger : Logger
{
    public override void Log(string message)
    {
        // 先写文件
        File.AppendAllText("log.txt", $"[{DateTime.Now}] {message}\n");

        // 再调用父类的控制台输出(复用父类逻辑)
        base.Log(message);
    }
}

// 使用
FileLogger logger = new FileLogger();
logger.Log("用户登录成功");

// 效果:既写入了文件,又输出到了控制台

base 的常见用途

场景 示例
在重写的方法中调用父类版本 base.Log(message)
子类构造函数调用父类构造函数 public Dog(string name) : base(name)
访问被子类隐藏的父类成员 base.SomeField

7. 方法隐藏(new 关键字)—— 这不是多态!

什么是方法隐藏?

new 关键字在子类中定义一个和父类同名的非虚方法,这就叫"方法隐藏"。

它与多态的关键区别:隐藏时,调用哪个方法取决于变量的声明类型,而非实际类型。

public class Person
{
    public void SayHello()                    // 注意:没有 virtual
    {
        Console.WriteLine("你好,我是一个人。");
    }
}

public class Student : Person
{
    public new void SayHello()                // new:隐藏父类方法
    {
        Console.WriteLine("你好,我是一个学生。");
    }
}

// 关键对比:
Student s = new Student();
s.SayHello();          // 输出:你好,我是一个学生。

Person p = s;          // p 和 s 指向同一个对象!
p.SayHello();          // 输出:你好,我是一个人。(调用的是父类版本!)

很诡异是吗?同一个对象,通过不同类型的变量去调用,结果不一样!

这就是 newoverride 的本质区别。


8. virtual vs new —— 一张表彻底搞清

假设有三个角色:Person(父类)、Student(子类)、"调用者"。

场景一:virtual + override(真正的多态)

public class Person
{
    public virtual void SayHello()
    {
        Console.WriteLine("你好,我是一个人。");
    }
}

public class Student : Person
{
    public override void SayHello()
    {
        Console.WriteLine("你好,我是一个学生。");
    }
}

Student s = new Student();
Person p = s;

s.SayHello();   // 输出:你好,我是一个学生。
p.SayHello();   // 输出:你好,我是一个学生。(多态!)

场景二:new(方法隐藏)

public class Person
{
    public void SayHello()                      // 没有 virtual
    {
        Console.WriteLine("你好,我是一个人。");
    }
}

public class Student : Person
{
    public new void SayHello()
    {
        Console.WriteLine("你好,我是一个学生。");
    }
}

Student s = new Student();
Person p = s;

s.SayHello();   // 输出:你好,我是一个学生。
p.SayHello();   // 输出:你好,我是一个人。(因为 p 的类型是 Person)

对比总结表

virtual + override new(方法隐藏)
绑定方式 动态绑定(运行时) 静态绑定(编译时)
决定因素 对象的实际类型 变量的声明类型
是否多态 ✅ 是多态 ❌ 不是多态
何时使用 希望子类有不同的行为 父类方法无法标记 virtual(如第三方库),但又想"同名"
推荐程度 ⭐⭐⭐⭐⭐ 强烈推荐 ⭐ 不推荐,容易造成混淆

判断口诀

  • virtual + override运行时决定,看对象"本质上是什么"
  • new编译时决定,看变量"名义上是什么"

9. 抽象类与多态

抽象类是什么?

如果父类的方法根本没办法给出默认实现,就把它标记为 abstract(抽象)。

// abstract 类:不能直接 new
public abstract class Shape
{
    public string Name { get; set; }

    // abstract 方法:没有方法体,子类必须实现
    public abstract double GetArea();

    // 抽象类中也可以有普通方法
    public void PrintInfo()
    {
        Console.WriteLine($"形状:{Name},面积:{GetArea():F2}");
    }
}

关键规则

规则 说明
抽象类不能 new Shape s = new Shape();
抽象方法没有方法体 不能写 { },直接分号结尾
抽象方法只能在抽象类中 普通类里不能有 abstract 方法
子类必须实现所有抽象方法 除非子类自己也是抽象类

示例

public abstract class Animal
{
    public abstract void MakeSound();     // 抽象方法:每种动物叫声不同
    public void Sleep()                    // 普通方法:所有动物都要睡觉
    {
        Console.WriteLine("Zzz...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()       // 必须实现!
    {
        Console.WriteLine("汪汪汪!");
    }
}

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

// 使用多态
Animal[] animals = { new Dog(), new Cat() };
foreach (var a in animals)
{
    a.MakeSound();   // 多态:各自叫各自的声音
    a.Sleep();        // 普通方法:都是 "Zzz..."
}

virtual vs abstract 对比

virtual(虚方法) abstract(抽象方法)
所在类 普通类 抽象类
是否有默认实现 ✅ 有方法体 ❌ 没有方法体
子类是否必须重写 ❌ 可选 ✅ 必须(除非子类也是抽象类)
语义 "你可以改" "你必须自己写"
比喻 给你一份参考答案 给你一张白纸

10. 接口与多态

接口是比抽象类更"纯"的多态工具。接口只定义"能做什么",不涉及"怎么做"。

// 定义接口
public interface IFlyable
{
    void Fly();    // 接口方法默认就是 public abstract,不需要写关键字
}

public interface ISwimmable
{
    void Swim();
}

// 一个类可以实现多个接口
public class Duck : IFlyable, ISwimmable
{
    public void Fly()
    {
        Console.WriteLine("鸭子扑腾着翅膀飞起来了!");
    }

    public void Swim()
    {
        Console.WriteLine("鸭子在水面上悠闲地游着...");
    }
}

public class Fish : ISwimmable
{
    public void Swim()
    {
        Console.WriteLine("鱼儿在水下穿梭!");
    }
}

// 多态:用接口类型引用
List<ISwimmable> swimmers = new List<ISwimmable>
{
    new Duck(),
    new Fish()
};

foreach (var s in swimmers)
{
    s.Swim();   // 同一个 Swim,不同的表现!
}

/* 输出:
   鸭子在水面上悠闲地游着...
   鱼儿在水下穿梭!
*/

面向接口编程 —— 多态的终极形态

// 定义"支付"接口
public interface IPayment
{
    void Pay(decimal amount);
}

// 不同的支付方式
public class Alipay : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"支付宝扫码支付:{amount} 元");
    }
}

public class WeChatPay : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"微信扫码支付:{amount} 元");
    }
}

public class CreditCard : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"信用卡刷卡支付:{amount} 元");
    }
}

// 收银台:不管什么支付方式,统一处理
public class Cashier
{
    public void Checkout(IPayment payment, decimal total)
    {
        Console.WriteLine($"应收:{total} 元");
        payment.Pay(total);    // 多态!不管什么方式,调 Pay 就行
        Console.WriteLine("支付完成!");
    }
}

// 使用
var cashier = new Cashier();
cashier.Checkout(new Alipay(), 100);
cashier.Checkout(new WeChatPay(), 200);
cashier.Checkout(new CreditCard(), 300);

// 如果以后新增了"数字货币支付":
// 1. 建一个类实现 IPayment
// 2. Cashier 类一行代码不用改!

11. 实战案例:游戏角色系统

下面用多态做一个简单的 RPG 战斗系统,感受多态在实际项目中的威力。

using System;
using System.Collections.Generic;

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

    public Character(string name, int hp, int attack, int defense)
    {
        Name = name;
        HP = hp;
        Attack = attack;
        Defense = defense;
    }

    // 抽象方法:每种角色攻击方式不同
    public abstract void UseSkill(Character target);

    // 虚方法:受伤逻辑(子类可以有自己的受伤表现)
    public virtual void TakeDamage(int damage)
    {
        int actualDamage = Math.Max(damage - Defense, 1);
        HP -= actualDamage;
        Console.WriteLine($"  {Name} 受到 {actualDamage} 点伤害,剩余 HP:{HP}");
    }

    // 普通方法:检查是否存活
    public bool IsAlive() => HP > 0;

    public void ShowStatus()
    {
        Console.WriteLine($"【{Name}】HP: {HP} | 攻击力: {Attack} | 防御力: {Defense}");
    }
}

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

    public override void UseSkill(Character target)
    {
        Console.WriteLine($"\n⚔️ {Name}(战士)使用【重击】!");
        int damage = Attack * 2;
        target.TakeDamage(damage);
    }
}

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

    public override void UseSkill(Character target)
    {
        Console.WriteLine($"\n🔥 {Name}(法师)释放【火球术】!");
        int damage = Attack + 30;
        target.TakeDamage(damage);
    }

    // 法师防御弱,受伤更多
    public override void TakeDamage(int damage)
    {
        int actualDamage = Math.Max(damage - Defense + 10, 1);
        HP -= actualDamage;
        Console.WriteLine($"  {Name}(法师护盾薄弱!)受到 {actualDamage} 点伤害,剩余 HP:{HP}");
    }
}

// ==================== 牧师 ====================
public class Priest : Character
{
    public Priest(string name)
        : base(name, 220, 30, 20) { }

    public override void UseSkill(Character target)
    {
        Console.WriteLine($"\n💚 {Name}(牧师)使用【治愈术】!");
        int heal = 50;
        target.HP += heal;
        Console.WriteLine($"  {target.Name} 恢复 {heal} 点生命值,当前 HP:{target.HP}");
    }
}

// ==================== 战斗系统(使用多态) ====================
public class BattleSystem
{
    public void StartBattle(Character teamA, Character teamB)
    {
        Console.WriteLine("========== 战斗开始! ==========\n");
        teamA.ShowStatus();
        teamB.ShowStatus();

        int round = 1;
        while (teamA.IsAlive() && teamB.IsAlive())
        {
            Console.WriteLine($"\n--- 第 {round} 回合 ---");

            // 多态!不管什么角色,统一调用 UseSkill
            teamA.UseSkill(teamB);
            if (!teamB.IsAlive()) break;

            teamB.UseSkill(teamA);

            teamA.ShowStatus();
            teamB.ShowStatus();
            round++;

            if (round > 20)
            {
                Console.WriteLine("\n战斗超时,平局!");
                break;
            }
        }

        Console.WriteLine("\n========== 战斗结束 ==========");
        if (teamA.IsAlive())
            Console.WriteLine($"{teamA.Name} 胜利!");
        else
            Console.WriteLine($"{teamB.Name} 胜利!");
    }
}

// ==================== 运行 ====================
class Program
{
    static void Main()
    {
        var warrior = new Warrior("亚瑟");
        var mage = new Mage("梅林");
        var priest = new Priest("安吉拉");

        // 战斗1:战士 vs 法师
        var battle = new BattleSystem();
        battle.StartBattle(warrior, mage);
    }
}

这个案例展示了多态的三个层次

  1. UseSkill() 是抽象的 —— 每种角色必须定义自己的技能
  2. TakeDamage() 是虚方法 —— 牧师的防御和法师不一样
  3. BattleSystem 只依赖 Character 类型 —— 新增角色不影响战斗逻辑

12. 实战案例:工资计算系统

这是一个贴近真实开发的多态案例。

using System;
using System.Collections.Generic;

// ==================== 员工基类 ====================
public abstract class Employee
{
    public string Name { get; set; }
    public string Id { get; set; }

    public Employee(string name, string id)
    {
        Name = name;
        Id = id;
    }

    // 抽象方法:计算月薪(不同类型员工计算方式不同)
    public abstract decimal CalculateSalary();

    // 虚方法:显示工资条(子类可以自定义格式)
    public virtual void PrintPayroll()
    {
        Console.WriteLine($"员工:{Name}({Id})");
        Console.WriteLine($"本月工资:{CalculateSalary():C}");
        Console.WriteLine("------------------------");
    }
}

// ==================== 固定月薪员工 ====================
public class SalariedEmployee : Employee
{
    public decimal MonthlySalary { get; set; }

    public SalariedEmployee(string name, string id, decimal monthlySalary)
        : base(name, id)
    {
        MonthlySalary = monthlySalary;
    }

    public override decimal CalculateSalary()
    {
        return MonthlySalary;
    }
}

// ==================== 时薪员工 ====================
public class HourlyEmployee : Employee
{
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }

    public HourlyEmployee(string name, string id, decimal hourlyRate, int hoursWorked)
        : base(name, id)
    {
        HourlyRate = hourlyRate;
        HoursWorked = hoursWorked;
    }

    public override decimal CalculateSalary()
    {
        // 超过160小时算加班,加班费1.5倍
        if (HoursWorked > 160)
        {
            int overtime = HoursWorked - 160;
            return 160 * HourlyRate + overtime * HourlyRate * 1.5m;
        }
        return HoursWorked * HourlyRate;
    }

    public override void PrintPayroll()
    {
        base.PrintPayroll();
        Console.WriteLine($"  工时:{HoursWorked} 小时,时薪:{HourlyRate:C}");
        if (HoursWorked > 160)
            Console.WriteLine($"  加班:{HoursWorked - 160} 小时");
        Console.WriteLine("------------------------");
    }
}

// ==================== 销售员工(底薪+提成) ====================
public class CommissionEmployee : Employee
{
    public decimal BaseSalary { get; set; }
    public decimal SalesAmount { get; set; }
    public decimal CommissionRate { get; set; }  // 提成比例

    public CommissionEmployee(string name, string id,
        decimal baseSalary, decimal salesAmount, decimal commissionRate)
        : base(name, id)
    {
        BaseSalary = baseSalary;
        SalesAmount = salesAmount;
        CommissionRate = commissionRate;
    }

    public override decimal CalculateSalary()
    {
        return BaseSalary + SalesAmount * CommissionRate;
    }

    public override void PrintPayroll()
    {
        base.PrintPayroll();
        Console.WriteLine($"  底薪:{BaseSalary:C},销售额:{SalesAmount:C},提成:{SalesAmount * CommissionRate:C}");
        Console.WriteLine("------------------------");
    }
}

// ==================== 工资系统(使用多态) ====================
public class PayrollSystem
{
    public void ProcessPayroll(List<Employee> employees)
    {
        decimal totalPayroll = 0;

        Console.WriteLine("========== 工资单 ==========\n");

        foreach (var emp in employees)
        {
            emp.PrintPayroll();          // 多态!不同类型的员工打印不同格式
            totalPayroll += emp.CalculateSalary();  // 多态!不同计算方式
        }

        Console.WriteLine($"\n本月工资总额:{totalPayroll:C}");
    }
}

// ==================== 运行 ====================
class Program
{
    static void Main()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee("张三", "E001", 15000),
            new HourlyEmployee("李四", "E002", 50, 180),
            new CommissionEmployee("王五", "E003", 5000, 100000, 0.05m),
            new SalariedEmployee("赵六", "E004", 20000),
        };

        var payroll = new PayrollSystem();
        payroll.ProcessPayroll(employees);
    }
}

/* 输出示例:
   ========== 工资单 ==========
   员工:张三(E001)
   本月工资:¥15,000.00
   ------------------------
   员工:李四(E002)
   本月工资:¥9,500.00
     工时:180 小时,时薪:¥50.00
     加班:20 小时
   ------------------------
   员工:王五(E003)
   本月工资:¥10,000.00
     底薪:¥5,000.00,销售额:¥100,000.00,提成:¥5,000.00
   ------------------------
   ...

   本月工资总额:¥54,500.00
*/

重点观察PayrollSystem.ProcessPayroll() 方法写完后,如果新增一种员工类型(比如"外包员工"),这个方法一行代码都不用改


13. 常见错误与避坑指南

错误1:父类没写 virtual,子类写了 override

public class Parent
{
    public void DoWork() { }        // ❌ 没有 virtual
}

public class Child : Parent
{
    public override void DoWork() { }  // ❌ 编译错误!父类方法不是 virtual
}

解决:给父类方法加 virtual(或者子类用 new 隐藏,但不推荐)。

错误2:签名不一致

public class Parent
{
    public virtual void DoWork(int x) { }
}

public class Child : Parent
{
    public override void DoWork(string x) { }  // ❌ 参数类型不一样!
}

解决:参数类型、个数、顺序、返回值类型必须完全一致。

错误3:子类重写的方法前面忘记写 override

public class Parent
{
    public virtual void DoWork() { }
}

public class Child : Parent
{
    public void DoWork() { }  // ⚠️ 没写 override!编译器给警告,行为相当于 new
}

后果:你以为实现了多态,实际上悄悄变成了方法隐藏。

错误4:通过父类引用访问子类独有的成员

public class Animal
{
    public virtual void Speak() { }
}

public class Dog : Animal
{
    public override void Speak() { }

    public void WagTail()           // 狗独有的方法
    {
        Console.WriteLine("摇尾巴~");
    }
}

Animal a = new Dog();
a.WagTail();     // ❌ 编译错误!Animal 类型没有 WagTail 方法

// 如果需要调用,要先转换类型
((Dog)a).WagTail();           // 方式一:强制转换(不安全)
(a as Dog)?.WagTail();        // 方式二:安全转换(推荐)
if (a is Dog dog)             // 方式三:模式匹配(最推荐)
    dog.WagTail();

14. 最佳实践

✅ 推荐做法

实践 说明 示例
优先使用接口 接口是"我能做什么",耦合度最低 IPaymentILoggerIDisposable
父类方法用 virtual 标记 预期子类需要自定义行为时 public virtual void Process()
子类重写用 override 明确表明是重写,而不是隐藏 public override void Process()
优先使用组合而非继承 不是所有关系都适合继承 车有引擎(组合) vs 车是交通工具(继承)
基类引用 + 多态遍历 替代大量 if-else 和 switch foreach (var item in list) item.DoWork();

❌ 不推荐做法

实践 原因
过度使用 new 隐藏方法 造成混淆,违反直觉
在构造函数中调用 virtual 方法 子类还没初始化完,可能出 bug
继承层次超过3层 难以理解和维护
用类型判断代替多态 if (x is Dog) ... else if (x is Cat) ... 违背多态初衷

15. 课后练习

练习1:交通工具系统

设计以下类结构,使用多态实现:

Vehicle(基类)
├── Car(汽车)
│   └── ElectricCar(电动车)
├── Bicycle(自行车)
└── Bus(公交车)

要求:

  • 基类有一个 virtual void Start() 方法
  • 每种子类给出不同的启动描述
  • 基类有一个 abstract decimal CalculateFare(int distance) 方法(计算费用)
  • Car 每公里2元,ElectricCar 每公里1元,Bus 每公里0.5元

练习2:通知系统

定义接口 INotifier,包含方法 void Send(string message, string recipient)。实现三种通知方式:

  • EmailNotifier:输出 "发送邮件给 {recipient}:{message}"
  • SmsNotifier:输出 "发送短信给 {recipient}:{message}"
  • WeChatNotifier:输出 "发送微信给 {recipient}:{message}"

然后用多态批量发送通知。

练习3:猜输出(考察 virtual vs new)

public class A
{
    public virtual void Print() => Console.WriteLine("A");
}

public class B : A
{
    public override void Print() => Console.WriteLine("B");
}

public class C : B
{
    public new void Print() => Console.WriteLine("C");
}

// 问:下面的代码分别输出什么?
A obj1 = new C();
obj1.Print();   // ???

B obj2 = new C();
obj2.Print();   // ???

C obj3 = new C();
obj3.Print();   // ???
点击查看答案
A obj1 = new C();
obj1.Print();   // 输出:B
// 解释:C 用 new 隐藏了 Print,所以多态链到 B 为止
// A 引用 → 找 virtual 链 → B 重写了 → C 是 new(不在链上)→ 调用 B 的

B obj2 = new C();
obj2.Print();   // 输出:B
// 同上,B 引用只能看到 B 的 override 版本

C obj3 = new C();
obj3.Print();   // 输出:C
// C 类型的变量,调的是 C 自己的 new 版本

16. 总结

一张图理解多态

              ┌─────────────┐
              │   基类(父类) │
              │  virtual 方法 │    ← "这个方法可以被子类改写"
              └──────┬──────┘
                     │ 继承
         ┌───────────┼───────────┐
         ↓           ↓           ↓
    ┌─────────┐ ┌─────────┐ ┌─────────┐
    │ 子类 A  │ │ 子类 B  │ │ 子类 C  │
    │override │ │override │ │override │    ← "我们用不同的方式实现"
    └─────────┘ └─────────┘ └─────────┘
         ↑           ↑           ↑
         └───────────┼───────────┘
                     │
         ┌─────────────────────┐
         │  基类引用变量        │
         │  foreach (var x ... │    ← "不管你是谁,用你的方式做"
         │      x.那个方法();  │
         └─────────────────────┘

核心概念速查表

概念 关键字 一句话解释
虚方法 virtual 父类说:你可以改我的方法
重写 override 子类说:我确实改了你的方法
方法隐藏 new 子类说:我和你同名,但跟你没关系
抽象方法 abstract 父类说:我不管怎么做,你必须自己写
抽象类 abstract class 不能直接 new 的类,只能被继承
密封 sealed 到此为止,别再重写了
基类调用 base 在子类中调用父类的版本
动态绑定 运行时决定调用哪个方法(virtual + override)
静态绑定 编译时决定调用哪个方法(new)

多态的三大价值

  1. 可替换性:子类对象可以随时替换父类对象,不影响调用方代码
  2. 可扩展性:新增功能 = 新增一个类,不需要修改已有代码
  3. 可维护性:逻辑集中在一处,修改方便

最后一句话:多态就是"用一样的接口做不一样的事"。掌握了多态,你就掌握了面向对象编程的灵魂。

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