什么是多态?
多态的英文单词是(Polymorphism),这个词来源于希腊语,"Poly" 意为 "多","Morph" 意为 "形态"。所以,多态的字面意思就是 "多种形态" 或 "多种形式"。
在面向对象编程中,多态性允许你使用一个父类或接口类型的变量来引用不同子类或实现类的对象,并且在调用该变量上的方法时,能够自动执行该对象所属的实际子类的特定实现。
简单来说,就是同一个方法调用,作用在不同的对象上,可以产生不同的行为结果。
回顾我们的例子:
- 我们有一个 Animal 基类,它有一个 virtual void MakeSound() 方法。
- 我们有 Dog 子类和 Cat 子类,它们都 override 了 MakeSound() 方法。
- 多态性意味着,即使我们只有一个 Animal 类型的变量,当它引用一个 Dog 对象时调用 MakeSound(),会执行狗叫;当它引用一个 Cat 对象时调用 MakeSound(),会执行猫叫。程序能够智能地选择正确的叫声!
多态的实现关键:继承与方法重写
多态不是凭空产生的,它依赖于我们之前学习的两个关键机制:
- 继承 (Inheritance): 必须存在一个基类(或接口)和至少一个派生类。子类继承了父类(或接口)的成员。
- 方法重写 (Method Overriding):
- 基类中必须有一个方法被声明为 virtual(表示允许被子类重写)或 abstract(表示必须被子类重写,后面会讲)。
- 子类中使用 override 关键字提供该方法的具体实现。
只有满足这两个条件,才能通过基类引用调用子类重写后的方法,从而实现多态。
多态的威力:通过基类引用操作不同子类对象
多态最强大的地方在于,你可以编写不依赖于具体子类类型的代码。你可以编写操作基类对象的代码,而这些代码可以无缝地适用于任何未来可能从该基类派生出来的新子类,只要新子类正确地重写了相关方法。
代码示例:使用 Animal 引用处理 Dog 和 Cat
// Program.cs (续 InheritanceDemo)
using System;
using System.Collections.Generic; // 引入 List
namespace InheritanceDemo
{
internal class Program
{
static void Main(string[] args)
{
// --- 创建不同子类的对象 ---
Animal myDog = new Dog("旺财", 2, "金毛"); // myDog 变量类型是 Animal,但实际指向 Dog 对象
Animal myCat = new Cat("咪咪", 1); // myCat 变量类型是 Animal,但实际指向 Cat 对象
Animal genericAnimal = new Animal("某种生物", 5); // 指向 Animal 对象
Console.WriteLine("--- 单独调用 MakeSound ---");
myDog.MakeSound(); // 输出: 旺财 (金毛) 正在汪汪叫! (执行 Dog 的实现)
myCat.MakeSound(); // 输出: 咪咪 正在喵喵叫! (执行 Cat 的实现)
genericAnimal.MakeSound(); // 输出: 某种生物 发出了 某种声音 (执行 Animal 的原始实现)
Console.WriteLine("\n--- 使用 List<Animal> 展示多态 ---");
// 创建一个可以存放 Animal 类型引用的列表
// 注意:List 的类型是基类 Animal
List<Animal> animals = new List<Animal>();
// 向列表中添加不同子类的对象
animals.Add(new Dog("小白", 3, "萨摩耶"));
animals.Add(new Cat("花花", 2));
animals.Add(new Dog("大黄", 4, "田园犬"));
animals.Add(new Animal("路人甲", 10)); // 也可以添加基类对象
// 遍历列表,对每个 Animal 调用 MakeSound
// 注意:循环变量 animal 的类型是 Animal
foreach (Animal animal in animals)
{
Console.Write($"{animal.Name}: ");
// 核心:这里调用 animal.MakeSound() 时,
// 程序会在运行时检查 animal 变量实际指向的对象类型 (Dog, Cat, 或 Animal)
// 然后调用该实际类型的 MakeSound() 实现!
animal.MakeSound();
}
Console.ReadKey();
}
}
// --- Animal, Dog, Cat 类定义 (保持不变,确保 MakeSound 是 virtual/override) ---
public class Animal { /* ... public virtual void MakeSound() ... */ }
public class Dog : Animal { /* ... public override void MakeSound() ... */ }
public class Cat : Animal { /* ... public override void MakeSound() ... */ }
}
输出结果会是:
--- 单独调用 MakeSound ---
旺财 (金毛) 正在汪汪叫!
咪咪 正在喵喵叫!
某种生物 发出了 某种声音
--- 使用 List<Animal> 展示多态 ---
小白: 小白 (萨摩耶) 正在汪汪叫!
花花: 花花 正在喵喵叫!
大黄: 大黄 (田园犬) 正在汪汪叫!
路人甲: 路人甲 发出了 某种声音
看到 foreach 循环中的神奇之处了吗?我们只写了一行 animal.MakeSound();,但对于列表中的每个不同类型的动物,它都自动执行了正确的叫声方法。这就是多态!
运行时决定行为
多态的关键在于,调用哪个版本的重写方法是在程序运行时 (Runtime),而不是编译时 (Compile time),根据变量实际引用的对象类型来决定的。这被称为晚期绑定 (Late Binding) 或 动态分派 (Dynamic Dispatch)。
编译器在编译时只检查 animal 变量(类型为 Animal)是否有权调用 MakeSound() 方法(因为它在 Animal 类中定义了 virtual 的 MakeSound)。但具体执行哪个 MakeSound(Dog 的、Cat 的还是 Animal 的)要等到程序运行起来,检查 animal 变量当时指向的是哪个类型的对象才能确定。
抽象类与抽象方法
有时,基类本身可能只是一个抽象的概念,没有具体的实例意义,或者基类中的某个方法无法提供一个通用的默认实现,必须由子类去具体实现。这时可以使用抽象类和抽象方法。
- 抽象类 (abstract class):
- 使用 abstract 关键字声明。
- 不能被实例化 (你不能 new 一个抽象类的对象,比如不能 new Animal() 如果 Animal 是抽象的)。
- 可以包含普通的(非抽象的)成员(字段、属性、方法)和构造函数。
- 可以包含抽象成员(通常是抽象方法)。
- 主要目的是作为基类,为派生类提供一个共同的抽象基础和部分实现。
- 抽象方法 (abstract method):
- 使用 abstract 关键字声明。
- 只能在抽象类中声明。
- 没有方法体(只有方法签名,后面直接跟分号 ;)。它只是一个“契约”,规定了子类必须提供这个方法的实现。
- 抽象方法隐式地是 virtual 的。
- 任何直接继承自包含抽象方法的抽象类的非抽象子类,都必须使用 override 关键字实现所有继承的抽象方法。
代码示例:将 Animal 改为抽象类,MakeSound 改为抽象方法
如果认为“动物”本身是一个抽象概念,而每种具体动物的叫声都不同且没有通用的“动物叫声”,可以这样修改:
// Animal.cs (修改为 abstract)
using System;
namespace InheritanceDemo
{
// 抽象基类
public abstract class Animal // 使用 abstract 关键字
{
public string Name { get; set; }
public int Age { get; set; }
// 构造函数仍然可以有,供子类调用
public Animal(string name, int age)
{
Console.WriteLine("Abstract Animal 构造函数被调用!");
this.Name = name;
this.Age = age;
}
// 抽象方法:只有声明,没有实现体,用分号结束
// 强制子类必须提供自己的实现
public abstract void MakeSound();
// 普通方法仍然可以有
public void Eat()
{
Console.WriteLine($"{Name} 正在吃东西...");
}
public void Sleep()
{
Console.WriteLine($"{Name} 正在睡觉...");
}
}
// Dog.cs (必须 override 抽象方法 MakeSound)
public class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, int age, string breed) : base(name, age) { /* ... */ }
// 必须使用 override 实现基类的抽象方法
public override void MakeSound()
{
Console.WriteLine($"{Name} ({Breed}) 正在汪汪叫!");
}
public void Bark() { MakeSound(); } // 可以继续这样用
}
// Cat.cs (同样必须 override 抽象方法 MakeSound)
public class Cat : Animal
{
public Cat(string name, int age) : base(name, age) { }
// 必须使用 override 实现基类的抽象方法
public override void MakeSound()
{
Console.WriteLine($"{Name} 正在喵喵叫!");
}
}
}
修改后的 Program.cs 注意点:
// Program.cs (使用抽象 Animal)
static void Main(string[] args)
{
// 编译错误!不能创建抽象类的实例
// Animal genericAnimal = new Animal("某种生物", 5);
// 仍然可以通过基类引用指向子类对象
Animal myDog = new Dog("旺财", 2, "金毛");
Animal myCat = new Cat("咪咪", 1);
myDog.MakeSound(); // 调用 Dog 的实现
myCat.MakeSound(); // 调用 Cat 的实现
List<Animal> animals = new List<Animal>();
animals.Add(new Dog("小白", 3, "萨摩耶"));
animals.Add(new Cat("花花", 2));
// animals.Add(new Animal("路人甲", 10)); // 编译错误!不能添加抽象类的实例
foreach (Animal animal in animals)
{
animal.MakeSound(); // 多态仍然有效!自动调用 Dog 或 Cat 的实现
}
Console.ReadKey();
}
virtual vs. abstract 方法总结:
特性 | virtual 方法 | abstract 方法 |
基类实现 | 必须有实现体(方法体) | 不能有实现体(只有声明) |
声明位置 | 可以在任何非 sealed 类中 | 只能在 abstract 类中 |
子类重写 | 子类可以 (非必须) 使用 override 重写 | 非抽象子类必须使用 override 实现 |
目的 | 提供一个默认实现,允许子类修改 | 定义一个契约,强制子类提供实现 |
到你动手了!
使用你在上一课创建的 Shape, Circle, Rectangle 类:
- 将 Shape 类改为 abstract。
- 将 Shape 类中的 Draw() 方法改为 abstract。 (删除其方法体,在声明后加分号)。
- 确保 Circle 和 Rectangle 类仍然使用 override 来实现 Draw() 方法。 (现在这是强制性的了)。
- 在 Program.cs 的 Main 方法中:
- 尝试创建一个 Shape 对象 (new Shape(...)),确认它会编译失败。
- 创建一个 List<Shape>。
- 向列表中添加几个 Circle 和 Rectangle 对象。
- 使用 foreach 循环遍历列表,对每个 Shape 类型的变量调用 Draw() 方法。
- 观察是否输出了正确的圆形或矩形的绘制信息,验证多态是否工作。
Comments NOTHING