CSharp(二十五)类(Class)多态
目录
- 什么是多态 —— 从小明的遥控器说起
- 为什么需要多态 —— 没有多态的世界有多痛苦
- 多态的底层机制 —— 三要素缺一不可
- virtual 关键字详解
- override 关键字详解
- base 关键字 —— 在子类中调用父类的方法
- 方法隐藏(new 关键字)—— 这不是多态!
- virtual vs new —— 一张表彻底搞清
- 抽象类与多态
- 接口与多态
- 实战案例:游戏角色系统
- 实战案例:工资计算系统
- 常见错误与避坑指南
- 最佳实践
- 课后练习
- 总结
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 一个对象丢进列表
// 主逻辑代码一行都不用改!
多态的核心价值:
- 可扩展性:新增类型不影响已有代码(开闭原则)
- 统一处理:可以用一个循环处理不同类型
- 代码简洁:消除大量 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(); // 输出:你好,我是一个人。(调用的是父类版本!)
很诡异是吗?同一个对象,通过不同类型的变量去调用,结果不一样!
这就是 new 和 override 的本质区别。
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);
}
}
这个案例展示了多态的三个层次:
UseSkill()是抽象的 —— 每种角色必须定义自己的技能TakeDamage()是虚方法 —— 牧师的防御和法师不一样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. 最佳实践
✅ 推荐做法
| 实践 | 说明 | 示例 |
|---|---|---|
| 优先使用接口 | 接口是"我能做什么",耦合度最低 | IPayment、ILogger、IDisposable |
| 父类方法用 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) |
多态的三大价值
- 可替换性:子类对象可以随时替换父类对象,不影响调用方代码
- 可扩展性:新增功能 = 新增一个类,不需要修改已有代码
- 可维护性:逻辑集中在一处,修改方便
最后一句话:多态就是"用一样的接口做不一样的事"。掌握了多态,你就掌握了面向对象编程的灵魂。