18-继承和多态
继承和多态是面向对象编程的重要特性。继承允许一个类从另一个类获得成员,多态允许同一操作在不同对象上有不同的行为。
一、继承(Inheritance)
1. 基本概念
通过继承,子类(派生类)可以获取父类(基类)的成员,并添加新成员或重写父类方法。
csharp
// 基类(父类)
public class Animal
{
public string Name { get; set; }
public void Eat()
{
Console.WriteLine($"{Name} 在吃东西");
}
}
// 派生类(子类)
public class Dog : Animal // 使用 : 表示继承
{
public void Bark()
{
Console.WriteLine($"{Name} 在汪汪叫"); // Name 从 Animal 继承
}
}2. 继承的特性
| 特性 | 说明 |
|---|---|
| 单继承 | C# 类只能继承一个直接基类 |
| 传递性 | A → B → C,C 拥有 A 和 B 的成员 |
| 可访问成员 | 子类可访问父类的 public 和 protected 成员 |
| 不继承 | 构造函数、析构函数、静态构造函数 |
| sealed 类 | 不能被继承 |
3. 继承链的构造函数调用
csharp
public class Animal
{
public string Name { get; set; }
public Animal()
{
Console.WriteLine("Animal 默认构造函数");
}
public Animal(string name)
{
Name = name;
Console.WriteLine($"Animal 参数构造函数:{name}");
}
}
public class Dog : Animal
{
public string Breed { get; set; }
// 默认调用父类的无参构造函数
public Dog()
{
Console.WriteLine("Dog 默认构造函数");
}
// 指定调用父类的哪个构造函数
public Dog(string name, string breed) : base(name) // 调用 Animal(string)
{
Breed = breed;
Console.WriteLine($"Dog 参数构造函数:{name}, {breed}");
}
}
// 执行顺序:
// 1. 父类构造函数
// 2. 子类构造函数
var dog = new Dog("旺财", "金毛");
// 输出:
// Animal 参数构造函数:旺财
// Dog 参数构造函数:旺财, 金毛4. base 关键字
base 用于在子类中访问父类成员。
csharp
public class Employee
{
public string Name { get; set; }
public decimal BaseSalary { get; set; }
public Employee(string name, decimal baseSalary)
{
Name = name;
BaseSalary = baseSalary;
}
public virtual decimal CalculateSalary()
{
return BaseSalary;
}
}
public class Manager : Employee
{
public decimal Bonus { get; set; }
public Manager(string name, decimal baseSalary, decimal bonus)
: base(name, baseSalary) // 调用父类构造函数
{
Bonus = bonus;
}
public override decimal CalculateSalary()
{
// 调用父类方法 + 额外逻辑
return base.CalculateSalary() + Bonus;
}
}
var mgr = new Manager("张三", 10000, 5000);
Console.WriteLine($"总工资:{mgr.CalculateSalary()}"); // 150005. 密封类(sealed)
csharp
public sealed class FinalClass
{
// 这个类不能被继承
}
// 编译错误
// public class Derived : FinalClass { }二、多态(Polymorphism)
多态通过 虚方法(virtual) 和 重写(override) 实现。
1. 虚方法和重写
csharp
public class Animal
{
public string Name { get; set; }
// virtual:允许子类重写
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
public class Dog : Animal
{
// override:重写父类方法
public override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("喵喵喵");
}
}
public class Duck : Animal
{
// 没有重写——会使用父类的默认实现
}2. 多态的威力
csharp
// 父类类型引用子类对象
Animal[] animals = new Animal[]
{
new Dog { Name = "旺财" },
new Cat { Name = "咪咪" },
new Duck { Name = "唐老鸭" },
new Dog { Name = "来福" }
};
// 同一行代码,不同行为
foreach (Animal animal in animals)
{
Console.Write($"{animal.Name}:");
animal.MakeSound();
}
// 输出:
// 旺财:汪汪汪
// 咪咪:喵喵喵
// 唐老鸭:动物发出声音(没有重写,使用父类默认)
// 来福:汪汪汪3. 方法隐藏(new 关键字)
csharp
public class BaseClass
{
public void Show()
{
Console.WriteLine("父类方法");
}
}
public class DerivedClass : BaseClass
{
public new void Show() // 隐藏(不是重写)
{
Console.WriteLine("子类方法");
}
}
// 重点:隐藏 vs 重写的区别
BaseClass obj1 = new DerivedClass();
obj1.Show(); // 输出:父类方法(没有多态行为!)
DerivedClass obj2 = new DerivedClass();
obj2.Show(); // 输出:子类方法virtual / override / new 对比
| 关键字 | 使用位置 | 效果 |
|---|---|---|
virtual | 父类方法 | 声明方法可以被重写 |
override | 子类方法 | 重写父类的 virtual 方法(实现多态) |
new | 子类方法 | 隐藏父类方法(不实现多态) |
sealed override | 子类方法 | 重写后禁止更下层的子类再重写 |
三、抽象类(Abstract Class)
抽象类使用 abstract 声明,不能实例化,必须被继承。可以包含抽象方法(无实现)和普通方法。
csharp
public abstract class Shape
{
public string Color { get; set; }
// 抽象方法:没有方法体,子类必须重写
public abstract double GetArea();
// 抽象属性
public abstract string ShapeType { get; }
// 普通方法——有实现
public void Display()
{
Console.WriteLine($"{ShapeType}:颜色={Color},面积={GetArea():F2}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override string ShapeType => "圆形";
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override string ShapeType => "矩形";
public override double GetArea()
{
return Width * Height;
}
}
// 使用
// Shape s = new Shape(); // ❌ 不能实例化抽象类
Shape[] shapes = new Shape[]
{
new Circle { Color = "红", Radius = 5 },
new Rectangle { Color = "蓝", Width = 4, Height = 6 }
};
foreach (var shape in shapes)
{
shape.Display();
}
// 输出:
// 圆形:颜色=红,面积=78.54
// 矩形:颜色=蓝,面积=24.00抽象类与普通类的区别
| 对比项 | 抽象类 | 普通类 |
|---|---|---|
new 实例化 | ❌ 不能 | ✅ 能 |
| 抽象方法 | 可以有 | 不能有 |
| 普通方法 | 可以有 | 可以有 |
| 子类要求 | 子类必须实现所有抽象成员 | 子类可选择重写 virtual 方法 |
四、类型检查和转换
is 运算符
检查对象是否是某个类型(或实现了某个接口)。
csharp
Animal animal = new Dog();
if (animal is Dog)
{
Console.WriteLine("这是一只狗");
}
if (animal is Animal)
{
Console.WriteLine("这是动物");
}
// C# 7.0+:模式匹配
if (animal is Dog dog)
{
Console.WriteLine($"狗名:{dog.Name}");
}
// switch 模式匹配
string GetAnimalSound(Animal animal)
{
return animal switch
{
Dog => "汪汪",
Cat => "喵喵",
_ => "未知声音"
};
}as 运算符
安全类型转换,失败返回 null(不抛异常)。
csharp
Animal animal = new Dog();
// as 转换
Dog? dog = animal as Dog;
if (dog != null)
{
dog.Bark(); // 安全使用
}
// as 转换失败返回 null
Cat? cat = animal as Cat;
Console.WriteLine(cat == null); // True(animal 不是 Cat)is vs as vs 强制转换
| 方式 | 失败时行为 | 适用场景 |
|---|---|---|
is 检查 | 不转换,返回 bool | 只需要判断类型 |
as 转换 | 返回 null | 安全的类型转换 |
强制转换 (T)obj | 抛出异常 | 确定类型一定正确时 |
五、里氏替换原则(LSP)
Liskov Substitution Principle: 子类对象应能替换父类对象使用,且程序行为不变。
csharp
// 符合 LSP 的设计
public class Bird
{
public virtual void Move()
{
Console.WriteLine("移动");
}
}
public class Sparrow : Bird
{
public override void Move()
{
Console.WriteLine("飞翔");
}
}
// 不符合 LSP 的设计
public class Penguin : Bird
{
public override void Move()
{
throw new NotSupportedException("企鹅不会飞"); // 违反了 LSP!
}
}六、密封方法
sealed 不仅可以作用于类,还可以作用于重写的方法,禁止更下层的子类再次重写。
csharp
public class Animal
{
public virtual void Eat() { }
}
public class Mammal : Animal
{
public sealed override void Eat() // 禁止后续子类重写
{
Console.WriteLine("哺乳动物在吃东西");
}
}
public class Dog : Mammal
{
// public override void Eat() { } // ❌ 编译错误:Eat 已被密封
}七、综合案例:员工薪资系统
csharp
// 抽象基类
public abstract class Employee
{
public string Name { get; set; }
public string EmployeeId { get; set; }
protected Employee(string name, string employeeId)
{
Name = name;
EmployeeId = employeeId;
}
public abstract decimal CalculateMonthlySalary(); // 抽象方法
public abstract string Position { get; } // 抽象属性
public virtual void DisplayInfo() // 虚方法(可重写)
{
Console.WriteLine($"\n=== {Position} ===");
Console.WriteLine($"姓名:{Name}");
Console.WriteLine($"工号:{EmployeeId}");
Console.WriteLine($"月薪:{CalculateMonthlySalary():C}");
}
}
// 正式员工
public class FullTimeEmployee : Employee
{
public decimal BaseSalary { get; set; }
public decimal Bonus { get; set; }
public FullTimeEmployee(string name, string id, decimal baseSalary, decimal bonus = 0)
: base(name, id)
{
BaseSalary = baseSalary;
Bonus = bonus;
}
public override string Position => "正式员工";
public override decimal CalculateMonthlySalary() => BaseSalary + Bonus;
public override void DisplayInfo()
{
base.DisplayInfo();
Console.WriteLine($"底薪:{BaseSalary:C},奖金:{Bonus:C}");
}
}
// 兼职员工
public class PartTimeEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public PartTimeEmployee(string name, string id, decimal hourlyRate, int hoursWorked)
: base(name, id)
{
HourlyRate = hourlyRate;
HoursWorked = hoursWorked;
}
public override string Position => "兼职员工";
public override decimal CalculateMonthlySalary() => HourlyRate * HoursWorked;
}
// 实习生
public class Intern : Employee
{
public decimal Stipend { get; set; }
public Intern(string name, string id, decimal stipend) : base(name, id)
{
Stipend = stipend;
}
public override string Position => "实习生";
public override decimal CalculateMonthlySalary() => Stipend;
// 不使用 override,使用 new 隐藏父类方法
public new void DisplayInfo()
{
Console.WriteLine($"\n=== {Position} ===");
Console.WriteLine($"姓名:{Name}");
Console.WriteLine($"月薪:{CalculateMonthlySalary():C}(实习津贴)");
}
}
// 使用多态
class Program
{
static void Main()
{
List<Employee> employees = new List<Employee>
{
new FullTimeEmployee("张三", "E001", 10000, 3000),
new PartTimeEmployee("李四", "E002", 80, 120),
new Intern("王五", "E003", 2000),
new FullTimeEmployee("赵六", "E004", 15000, 5000)
};
Console.WriteLine("===== 员工薪资报表 =====");
decimal total = 0;
foreach (var emp in employees)
{
emp.DisplayInfo();
total += emp.CalculateMonthlySalary();
}
Console.WriteLine($"\n总薪资支出:{total:C}");
// 特定类型员工的操作
foreach (var emp in employees)
{
if (emp is FullTimeEmployee fullTime && fullTime.Bonus > 2000)
{
Console.WriteLine($"\n{fullTime.Name} 获得高额奖金 {fullTime.Bonus:C}");
}
}
}
}核心知识点总结
关键字速查
| 关键字 | 用途 |
|---|---|
: | 继承/实现接口语法 |
base | 在子类中访问父类成员 |
virtual | 声明可重写的方法 |
override | 重写父类方法 |
new | 隐藏父类方法 |
abstract | 声明抽象类/抽象方法 |
sealed | 密封类(禁止继承)或密封方法(禁止继续重写) |
设计决策
| 场景 | 使用 |
|---|---|
| 需要共享基础实现 | 普通基类 |
| 需要强制子类实现某些方法 | 抽象类 |
| 只需要定义能力/合约 | 接口(见下一章) |
| 禁止被继承 | sealed 类 |
| 需要多态行为 | virtual + override |
注意事项
- C# 只支持单继承——一个类只能有一个父类
- 构造函数不可继承——子类必须通过
base()调用父类构造函数 override才有多态——new关键字只是隐藏,不是重写- 抽象类不能实例化——但可以有构造函数(用于子类调用)
- 遵循 LSP——子类应能无缝替换父类
- 优先使用组合而非继承——继承层级过深会增加维护成本


