多态
多态概述
多态(Polymorphism)是面向对象编程的核心特性之一,指同一接口在不同对象上表现出不同行为。C# 中多态分为两种:
- 运行时多态:通过
virtual/override实现动态派发,调用哪个方法在运行时决定 - 编译时多态:通过方法重载实现,编译器根据参数签名选择调用目标(已在第 14 课介绍)
本课重点讲解运行时多态,它是面向对象设计中最关键的能力。
virtual 与 override
基类用 virtual 标记一个方法为"可重写",派生类用 override 提供新实现。运行时根据对象的实际类型(而非变量声明类型)决定调用哪个方法,这就是动态派发。
示例
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Program
{
static void Main()
{
Animal a = new Dog();
a.Speak();
Animal b = new Cat();
b.Speak();
}
}
Dog barks
Cat meows
变量声明为 Animal,但实际对象是 Dog 和 Cat,运行时调用了各自重写后的版本——这就是多态的核心效果。
多态的实际应用:统一接口不同实现
多态最常见的场景是用基类引用数组或集合管理不同派生类对象,统一调用接口,各对象自行响应。
示例
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal makes a sound");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
class Cow : Animal
{
public override void Speak()
{
Console.WriteLine("Cow moos");
}
}
class Program
{
static void MakeThemSpeak(Animal[] animals)
{
foreach (Animal a in animals)
{
a.Speak();
}
}
static void Main()
{
Animal[] animals = { new Dog(), new Cat(), new Cow() };
MakeThemSpeak(animals);
}
}
Dog barks
Cat meows
Cow moos
MakeThemSpeak 方法只依赖 Animal 类型,无需知道具体是哪种动物,新增派生类也不需要修改此方法。
override 与 new 的区别
派生类中使用 new 关键字可以隐藏基类方法,但这不参与多态。override 重写参与动态派发,new 隐藏只在编译时按声明类型选择方法。
示例
using System;
class Base
{
public virtual void Print()
{
Console.WriteLine("Base.Print");
}
}
class OverrideDerived : Base
{
public override void Print()
{
Console.WriteLine("OverrideDerived.Print");
}
}
class NewDerived : Base
{
public new void Print()
{
Console.WriteLine("NewDerived.Print");
}
}
class Program
{
static void Main()
{
Base obj1 = new OverrideDerived();
obj1.Print();
Base obj2 = new NewDerived();
obj2.Print();
NewDerived obj3 = new NewDerived();
obj3.Print();
}
}
OverrideDerived.Print
Base.Print
NewDerived.Print
obj1:override重写,通过基类引用仍调用派生类版本obj2:new隐藏,通过基类引用调用的是基类版本,多态被破坏obj3:通过派生类引用才能访问new隐藏的版本
override,new 仅用于特殊情况下的方法隐藏。
abstract 方法与抽象类
abstract 方法没有实现,强制派生类必须重写。包含抽象方法的类本身也必须声明为 abstract,不能直接实例化。
示例
using System;
abstract class Shape
{
public abstract double Area();
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double Area()
{
return Width * Height;
}
}
class Program
{
static void PrintArea(Shape shape)
{
Console.WriteLine($"Area: {shape.Area():F2}");
}
static void Main()
{
Shape[] shapes = { new Circle(3), new Rectangle(4, 5) };
foreach (Shape s in shapes)
{
PrintArea(s);
}
}
}
Area: 28.27
Area: 20.00
抽象类定义了"是什么",派生类决定"怎么做",所有形状通过统一的 Area() 接口计算面积。
方法链:多层重写
virtual/override 可以沿继承链逐层重写,派生类可通过 base. 调用父类实现并在此基础上扩展。
示例
using System;
class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}
class Dog : Animal
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Dog barks");
}
}
class Puppy : Dog
{
public override void Speak()
{
base.Speak();
Console.WriteLine("Puppy yips");
}
}
class Program
{
static void Main()
{
Animal a = new Puppy();
a.Speak();
}
}
Animal speaks
Dog barks
Puppy yips
Puppy.Speak() 调用 base.Speak() 触发 Dog.Speak(),后者再调用 base.Speak() 触发 Animal.Speak(),形成从基类到派生类的链式输出。
ToString 重写
C# 中所有类型都继承自 object,其 ToString() 方法是 virtual 的。重写它可以让对象在输出时展示有意义的文本。
示例
using System;
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public Student(string name, int age)
{
Name = name;
Age = age;
}
public override string ToString()
{
return $"{Name} (Age: {Age})";
}
}
class Program
{
static void Main()
{
Student s = new Student("Alice", 20);
Console.WriteLine(s);
Console.WriteLine(s.ToString());
}
}
Alice (Age: 20)
Alice (Age: 20)
Console.WriteLine 内部自动调用 ToString(),重写后输出从默认类型名变为自定义格式。
里氏替换原则
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的基本原则:派生类对象必须能够替换其基类对象,而程序的行为不变。
示例
using System;
class Bird
{
public virtual void Fly()
{
Console.WriteLine("Bird flies");
}
}
class Swallow : Bird
{
public override void Fly()
{
Console.WriteLine("Swallow flies high");
}
}
class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguin cannot fly");
}
}
class Program
{
static void LetFly(Bird bird)
{
bird.Fly();
}
static void Main()
{
LetFly(new Swallow());
LetFly(new Penguin());
}
}
Swallow flies high
Unhandled Exception: System.NotSupportedException: Penguin cannot fly
Penguin 继承 Bird 但重写 Fly 抛出异常,破坏了基类"鸟会飞"的契约,违反 LSP。正确做法是重新设计继承层次,如将"飞行能力"提取为独立接口。
❓ 常见问题
virtual 方法可以不重写吗?override 方法还能再被重写吗?override 方法隐含 virtual 语义,派生类可继续重写。new 和 override 哪个更好?override,new 仅在必须隐藏且确认不需要多态时使用。ToString() 会怎样?Student 输出 Student。📖 小节
- 多态分为运行时(
virtual/override)和编译时(重载)两种 virtual标记可重写方法,override提供派生类新实现abstract方法强制派生类重写,抽象类不可实例化override参与动态派发,new隐藏基类方法破坏多态- 通过基类引用统一调用不同派生类方法是多态的典型应用
- 里氏替换原则要求派生类可完全替代基类使用
- 重写
ToString()是多态在日常编码中最常见的应用之一
📝 作业
- 定义
Vehicle基类,包含virtual void Move()方法,派生Car和Bicycle类重写Move(),在Main中用Vehicle[]数组统一调用 - 定义抽象类
Payment,包含abstract void Pay(decimal amount),派生CashPayment和CardPayment实现各自的支付逻辑 - 创建
Book类并重写ToString(),输出书名和价格,验证Console.WriteLine(book)的效果 - 编写代码演示
override和new的区别:用基类引用分别调用两个派生类对象,观察输出差异 - 思考:如果
Square继承Rectangle但重写SetWidth/SetHeight使边长始终相等,是否违反里氏替换原则?说明理由



