1多态是什么
多态的意思是:同一个行为,有多种不同的表现形式,教程概括成“一个接口,多个功能”。比如键盘上的 F1 是帮助键:在 Word 里按,弹出 Word 的帮助;在 Windows 桌面上按,弹出 Windows 的帮助和支持。同一个按键落在不同的软件上,结果不一样,这就是多态(教程里另一个 Flash 的例子已过时,Flash 在 2020 年底就停止支持了)。
2静态多态和动态多态
多态分两种:静态多态在编译的时候就定好调用哪个方法;动态多态要等程序运行的时候才决定执行哪个方法。
C# 里每个类型都是多态的,因为所有类型都继承自 Object(C# 里所有类型共同的祖先类)。下面的数组里放了整数、小数、字符串和一个 Circle 对象,对每个都调用 ToString:
| 11 | object[] items = { 42, 1.23, "Hello", new Circle() }; | // object 数组什么类型都能放;Circle 是个空类 |
| 12 | foreach (object x in items) | // 挨个取出数组里的元素,放进 x |
| 13 | { | // foreach 开始 |
| 14 | Console.WriteLine(x.ToString()); | // 同一句 ToString,交给不同的对象 |
| 15 | } | // foreach 结束 |
42 1.23 Hello Circle
四个都能打印:前三个打印自己的值,Circle 自己没写 ToString,用的是从 Object 继承来的版本,打印出类型名。
3静态多态:函数重载
在编译时就把方法调用和具体的方法连起来,叫早期绑定(也叫静态绑定)。C# 用函数重载和运算符重载实现它,运算符重载是下一章的内容。C# 里函数一般叫方法,所以函数重载也常叫方法重载。
重载:同一个范围里,好几个方法用同一个名字,但参数列表必须不同,要么参数类型不同,要么参数个数不同。
| 6 | public int Add(int a, int b, int c) | // 第一个 Add:接收三个整数 |
| 7 | { | // Add(三个参数) 开始 |
| 8 | return a + b + c; | // 返回三个数的和 |
| 9 | } | // Add(三个参数) 结束 |
| 10 | public int Add(int a, int b) | // 第二个 Add:同名,只接收两个整数 |
| 11 | { | // Add(两个参数) 开始 |
| 12 | return a + b; | // 返回两个数的和 |
| 13 | } | // Add(两个参数) 结束 |
| 20 | int add1 = dataClass.Add(1, 2); | // dataClass 是 TestData 对象;两个参数:1 + 2 = 3 |
| 21 | int add2 = dataClass.Add(1, 2, 3); | // 三个参数,走三个参数的 Add:1 + 2 + 3 = 6 |
视频里把它比成分拣台:同一块 Add 名牌下面有两个入口,一个三个孔、一个两个孔,调用时带几个参数,就只插得进孔数相同的那个入口。走哪个入口,编译器在编译时就定好了,运行结果是 add1 :3、add2 :6。
查看完整代码(27 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace PolymorphismApplication | // 定义命名空间 PolymorphismApplication |
| 3 | { | // 命名空间开始 |
| 4 | public class TestData | // 定义 TestData 类 |
| 5 | { | // TestData 类开始 |
| 6 | public int Add(int a, int b, int c) | // 第一个 Add:接收三个整数 |
| 7 | { | // Add(三个参数) 开始 |
| 8 | return a + b + c; | // 返回三个数的和 |
| 9 | } | // Add(三个参数) 结束 |
| 10 | public int Add(int a, int b) | // 第二个 Add:同名,参数个数不同 |
| 11 | { | // Add(两个参数) 开始 |
| 12 | return a + b; | // 返回两个数的和 |
| 13 | } | // Add(两个参数) 结束 |
| 14 | } | // TestData 类结束 |
| 15 | class Program | // 定义 Program 类 |
| 16 | { | // Program 类开始 |
| 17 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 18 | { | // Main 开始 |
| 19 | TestData dataClass = new TestData(); | // 创建一个 TestData 对象 |
| 20 | int add1 = dataClass.Add(1, 2); | // 两个参数,调用 Add(int, int):add1 = 3 |
| 21 | int add2 = dataClass.Add(1, 2, 3); | // 三个参数,调用 Add(int, int, int):add2 = 6 |
| 22 | | |
| 23 | Console.WriteLine("add1 :" + add1); | // 用 + 把文字和数字拼起来输出:add1 :3 |
| 24 | Console.WriteLine("add2 :" + add2); | // 输出 add2 :6 |
| 25 | } | // Main 结束 |
| 26 | } | // Program 类结束 |
| 27 | } | // 命名空间结束 |
4参数类型不同也是重载
Printdata 里有三个 print,参数个数一样,类型分别是 int、double、string,编译器按传进去的值是什么类型来挑。
| 22 | Printdata p = new Printdata(); | // 创建一个 Printdata 对象 |
| 23 | // 调用 print 来打印整数 | |
| 24 | p.print(1); | // 1 是 int,走 print(int i) |
| 25 | // 调用 print 来打印浮点数 | |
| 26 | p.print(1.23); | // 1.23 是 double,走 print(double f) |
| 27 | // 调用 print 来打印字符串 | |
| 28 | p.print("Hello Runoob"); | // 双引号里是 string,走 print(string s) |
查看完整输出(3 行)
输出整型: 1 输出浮点型: 1.23 输出字符串: Hello Runoob
查看完整代码(32 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace PolymorphismApplication | // 定义命名空间 PolymorphismApplication |
| 3 | { | // 命名空间开始 |
| 4 | class Printdata | // 定义 Printdata 类 |
| 5 | { | // Printdata 类开始 |
| 6 | void print(int i) | // 第一个 print:参数是 int |
| 7 | { | // print(int) 开始 |
| 8 | Console.WriteLine("输出整型: {0}", i ); | // {0} 是占位符,会换成后面的 i |
| 9 | } | // print(int) 结束 |
| 10 | | |
| 11 | void print(double f) | // 第二个 print:参数是 double(小数) |
| 12 | { | // print(double) 开始 |
| 13 | Console.WriteLine("输出浮点型: {0}" , f); | // 输出“输出浮点型: ”加上 f |
| 14 | } | // print(double) 结束 |
| 15 | | |
| 16 | void print(string s) | // 第三个 print:参数是 string(字符串) |
| 17 | { | // print(string) 开始 |
| 18 | Console.WriteLine("输出字符串: {0}", s); | // 输出“输出字符串: ”加上 s |
| 19 | } | // print(string) 结束 |
| 20 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 21 | { | // Main 开始 |
| 22 | Printdata p = new Printdata(); | // 创建一个 Printdata 对象 |
| 23 | // 调用 print 来打印整数 | |
| 24 | p.print(1); | // 1 是 int,走 print(int) |
| 25 | // 调用 print 来打印浮点数 | |
| 26 | p.print(1.23); | // 1.23 是 double,走 print(double) |
| 27 | // 调用 print 来打印字符串 | |
| 28 | p.print("Hello Runoob"); | // 字符串,走 print(string) |
| 29 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 30 | } | // Main 结束 |
| 31 | } | // Printdata 类结束 |
| 32 | } | // 命名空间结束 |
5只有返回类型不同,不能重载
在 TestData 里再写一个返回 double 的 Add,参数还是两个整数:
| 10 | public double Add(int a, int b) | // 新加的 Add:返回 double,参数是两个 int |
| 14 | public int Add(int a, int b) | // 原来的 Add:参数也是两个 int,编译器在这一行报错 |
Program.cs(14,20): error CS0111: 类型“TestData”已定义了一个名为“Add”的具有相同参数类型的成员
调用时只看参数,不看返回类型,两个 Add 的参数一模一样,编译器分不出该用哪一个。
6动态多态:抽象类
动态多态靠两样东西:抽象类和虚方法。抽象类用关键字 abstract 声明,里面可以有抽象方法:只写方法头(返回类型、名字、参数那一行)、不写方法体(花括号里的代码),具体怎么做留给派生类用 override(重写:在派生类里重新写这个方法)去实现。下面的 Shape 前面写了 abstract,Rectangle 继承 Shape。
| 6 | abstract public int area(); | // 抽象方法 area:分号结尾,没有 { } |
| 17 | public override int area () | // Rectangle 用 override 写出 area:返回宽 × 长 |
Rectangle 类的面积: 面积: 70
查看完整代码(34 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace PolymorphismApplication | // 定义命名空间 PolymorphismApplication |
| 3 | { | // 命名空间开始 |
| 4 | abstract class Shape | // 抽象类 Shape |
| 5 | { | // Shape 类开始 |
| 6 | abstract public int area(); | // 抽象方法 area:不写方法体 |
| 7 | } | // Shape 类结束 |
| 8 | class Rectangle: Shape | // Rectangle 继承 Shape |
| 9 | { | // Rectangle 类开始 |
| 10 | private int length; | // 字段:长 |
| 11 | private int width; | // 字段:宽 |
| 12 | public Rectangle( int a=0, int b=0) | // 构造函数,a=0 表示不传时默认是 0 |
| 13 | { | // Rectangle 构造函数开始 |
| 14 | length = a; | // 长 = a |
| 15 | width = b; | // 宽 = b |
| 16 | } | // Rectangle 构造函数结束 |
| 17 | public override int area () | // 用 override 实现抽象方法 area |
| 18 | { | // area 开始 |
| 19 | Console.WriteLine("Rectangle 类的面积:"); | // 打印提示 |
| 20 | return (width * length); | // 返回宽 × 长 |
| 21 | } | // area 结束 |
| 22 | } | // Rectangle 类结束 |
| 23 | | |
| 24 | class RectangleTester | // 定义 RectangleTester 类 |
| 25 | { | // RectangleTester 类开始 |
| 26 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 27 | { | // Main 开始 |
| 28 | Rectangle r = new Rectangle(10, 7); | // 造一个长 10、宽 7 的长方形 |
| 29 | double a = r.area(); | // 调用 area:打印提示,返回 7 × 10 = 70 |
| 30 | Console.WriteLine("面积: {0}",a); | // 输出“面积: 70” |
| 31 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 32 | } | // Main 结束 |
| 33 | } | // RectangleTester 类结束 |
| 34 | } | // 命名空间结束 |
抽象类可以包含抽象方法,一个都没有也行。
7抽象类的规则
下面每条规则都用编译器验证过。
- 不能直接 new 抽象类的对象。
| 28 | Shape s = new Shape(); | // 在 Main 里 new 抽象类 Shape |
Program.cs(28,20): error CS0144: 无法创建抽象类型或接口“Shape”的实例
- 抽象方法只能写在抽象类里面。 把 Shape 前面的
abstract去掉:
| 4 | class Shape | // 去掉了 abstract,Shape 成了普通类 |
| 5 | { | // Shape 类开始 |
| 6 | abstract public int area(); | // 抽象方法还留着,编译器在这一行报错 |
Program.cs(6,28): error CS0513: “Shape.area()”是抽象的,但它包含在非抽象类型“Shape”中
- 类前面写
sealed,这个类就被密封了,别的类不能再继承它。
| 8 | sealed class Rectangle: Shape | // sealed:Rectangle 被密封 |
| 24 | class Square : Rectangle | // Square 想继承被密封的 Rectangle |
Program.cs(24,19): error CS0509: “Square”: 无法从密封类型“Rectangle”派生
- 抽象类不能同时是 sealed。 抽象类等着别人继承,密封又不让继承,放在一起就矛盾了。
| 4 | abstract sealed class Shape | // 既抽象又密封 |
编译器报 CS0418:“Shape”: 抽象类型不能是密封的或静态的。后面还连带报了 Rectangle 无法从密封类型派生(CS0509)等错误。
8虚方法
基类里有个方法,想让每个派生类各写各的,就用关键字 virtual 把它声明成虚方法,派生类用 override 重写它。调用虚方法时执行哪个版本,要等运行时看对象实际是什么才决定。动态多态就是靠抽象类和虚方法实现的,后面要学的接口也能做到。
9例子:Draw
基类 Shape 有虚方法 Draw;Circle、Rectangle、Triangle 都重写了 Draw,先打印自己画什么,再用 base. 调用基类的版本。Main 里的 shapes 是一个 List<Shape>(能装 Shape 及其派生类对象的列表),依次放了一个 Rectangle、一个 Triangle、一个 Circle。
| 12 | public virtual void Draw() | // Shape 里的虚方法 Draw,打印“执行基类的画图任务” |
| 28 | public override void Draw() | // Rectangle 用 override 重写 Draw |
| 29 | { | // Rectangle.Draw 开始 |
| 30 | Console.WriteLine("画一个长方形"); | // 先打印自己画什么 |
| 31 | base.Draw(); | // base.Draw():再调用基类 Shape 的 Draw |
| 32 | } | // Rectangle.Draw 结束 |
| 56 | foreach (var shape in shapes) | // var 让编译器推断类型,这里 shape 是 Shape |
| 57 | { | // foreach 开始 |
| 58 | shape.Draw(); | // 同一句调用,执行的是对象自己那一版 Draw |
| 59 | } | // foreach 结束 |
视频里用调用台演示:每个对象分两层,下层继承自 Shape,上层是自己加的。shape 指向 Rectangle 时,先打印“画一个长方形”,base. 再打印“执行基类的画图任务”;三个对象共六行,最后是“按下任意键退出。”。调用台只是比喻,真实程序是运行时按对象的实际类型,找到它重写的那个方法。
查看完整输出(7 行)
画一个长方形 执行基类的画图任务 画一个三角形 执行基类的画图任务 画一个圆形 执行基类的画图任务 按下任意键退出。
查看完整代码(65 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Collections.Generic; | // 引入 List 所在的命名空间 |
| 3 | | |
| 4 | public class Shape | // 定义基类 Shape |
| 5 | { | // Shape 类开始 |
| 6 | public int X { get; private set; } | // X 是属性(后面会讲):外面只能读,不能改 |
| 7 | public int Y { get; private set; } | // Y 也是属性(后面会讲):外面只读 |
| 8 | public int Height { get; set; } | // Height 是属性(后面会讲):可读可写 |
| 9 | public int Width { get; set; } | // Width 是属性(后面会讲):可读可写 |
| 10 | | |
| 11 | // 虚方法 | |
| 12 | public virtual void Draw() | // 虚方法 Draw |
| 13 | { | // Shape.Draw 开始 |
| 14 | Console.WriteLine("执行基类的画图任务"); | // 基类版本打印的内容 |
| 15 | } | // Shape.Draw 结束 |
| 16 | } | // Shape 类结束 |
| 17 | | |
| 18 | class Circle : Shape | // Circle 继承 Shape |
| 19 | { | // Circle 类开始 |
| 20 | public override void Draw() | // 重写 Draw |
| 21 | { | // Circle.Draw 开始 |
| 22 | Console.WriteLine("画一个圆形"); | // 打印自己画什么 |
| 23 | base.Draw(); | // 再调用基类的 Draw |
| 24 | } | // Circle.Draw 结束 |
| 25 | } | // Circle 类结束 |
| 26 | class Rectangle : Shape | // Rectangle 继承 Shape |
| 27 | { | // Rectangle 类开始 |
| 28 | public override void Draw() | // 重写 Draw |
| 29 | { | // Rectangle.Draw 开始 |
| 30 | Console.WriteLine("画一个长方形"); | // 打印自己画什么 |
| 31 | base.Draw(); | // 再调用基类的 Draw |
| 32 | } | // Rectangle.Draw 结束 |
| 33 | } | // Rectangle 类结束 |
| 34 | class Triangle : Shape | // Triangle 继承 Shape |
| 35 | { | // Triangle 类开始 |
| 36 | public override void Draw() | // 重写 Draw |
| 37 | { | // Triangle.Draw 开始 |
| 38 | Console.WriteLine("画一个三角形"); | // 打印自己画什么 |
| 39 | base.Draw(); | // 再调用基类的 Draw |
| 40 | } | // Triangle.Draw 结束 |
| 41 | } | // Triangle 类结束 |
| 42 | | |
| 43 | class Program | // 定义 Program 类 |
| 44 | { | // Program 类开始 |
| 45 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 46 | { | // Main 开始 |
| 47 | // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle | |
| 48 | var shapes = new List<Shape> | // var:编译器按右边推断出 List<Shape> |
| 49 | { | // 列表初始化开始 |
| 50 | new Rectangle(), | // 第 1 个:长方形 |
| 51 | new Triangle(), | // 第 2 个:三角形 |
| 52 | new Circle() | // 第 3 个:圆形 |
| 53 | }; | // 列表初始化结束 |
| 54 | | |
| 55 | // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法 | |
| 56 | foreach (var shape in shapes) | // 依次取出列表里的对象 |
| 57 | { | // foreach 开始 |
| 58 | shape.Draw(); | // 按对象的实际类型执行它的 Draw |
| 59 | } | // foreach 结束 |
| 60 | | |
| 61 | Console.WriteLine("按下任意键退出。"); | // 最后打印退出提示 |
| 62 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 63 | } | // Main 结束 |
| 64 | | |
| 65 | } | // Program 类结束 |
10例子:CallArea
基类 Shape 的虚方法 area 返回 0,Rectangle 返回宽乘高,Triangle 返回宽乘高再除以二。Caller 类的 CallArea 方法,参数类型是 Shape。Main 里 c 是 Caller 对象,r 是 10 × 7 的 Rectangle,t 是 10 × 5 的 Triangle。
| 47 | a = sh.area(); | // CallArea 里:执行 sh 实际指向的对象的 area |
| 59 | c.CallArea(r); | // 传入 Rectangle:执行它的 area,10 × 7 = 70 |
| 60 | c.CallArea(t); | // 传入 Triangle:执行它的 area,10 × 5 / 2 = 25 |
查看完整输出(4 行)
Rectangle 类的面积: 面积: 70 Triangle 类的面积: 面积: 25
参数写的是 Shape,传进 Triangle,执行的仍是 Triangle 自己的 area,所以是 25 不是 0。
查看完整代码(64 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace PolymorphismApplication | // 定义命名空间 PolymorphismApplication |
| 3 | { | // 命名空间开始 |
| 4 | class Shape | // 定义基类 Shape(普通类) |
| 5 | { | // Shape 类开始 |
| 6 | protected int width, height; | // protected:派生类里也能用的宽和高 |
| 7 | public Shape( int a=0, int b=0) | // Shape 构造函数,默认值都是 0 |
| 8 | { | // Shape 构造函数开始 |
| 9 | width = a; | // 宽 = a |
| 10 | height = b; | // 高 = b |
| 11 | } | // Shape 构造函数结束 |
| 12 | public virtual int area() | // 虚方法 area |
| 13 | { | // Shape.area 开始 |
| 14 | Console.WriteLine("父类的面积:"); | // 基类版本的提示 |
| 15 | return 0; | // 基类不知道形状,返回 0 |
| 16 | } | // Shape.area 结束 |
| 17 | } | // Shape 类结束 |
| 18 | class Rectangle: Shape | // Rectangle 继承 Shape |
| 19 | { | // Rectangle 类开始 |
| 20 | public Rectangle( int a=0, int b=0): base(a, b) | // : base(a, b) 把参数交给 Shape 的构造函数 |
| 21 | { | // Rectangle 构造函数开始 |
| 22 | | |
| 23 | } | // Rectangle 构造函数结束 |
| 24 | public override int area () | // 重写 area |
| 25 | { | // Rectangle.area 开始 |
| 26 | Console.WriteLine("Rectangle 类的面积:"); | // 打印提示 |
| 27 | return (width * height); | // 返回宽 × 高 |
| 28 | } | // Rectangle.area 结束 |
| 29 | } | // Rectangle 类结束 |
| 30 | class Triangle: Shape | // Triangle 继承 Shape |
| 31 | { | // Triangle 类开始 |
| 32 | public Triangle(int a = 0, int b = 0): base(a, b) | // 同样把参数交给 Shape 的构造函数 |
| 33 | { | // Triangle 构造函数开始 |
| 34 | | |
| 35 | } | // Triangle 构造函数结束 |
| 36 | public override int area() | // 重写 area |
| 37 | { | // Triangle.area 开始 |
| 38 | Console.WriteLine("Triangle 类的面积:"); | // 打印提示 |
| 39 | return (width * height / 2); | // 返回宽 × 高 ÷ 2 |
| 40 | } | // Triangle.area 结束 |
| 41 | } | // Triangle 类结束 |
| 42 | class Caller | // 定义 Caller 类 |
| 43 | { | // Caller 类开始 |
| 44 | public void CallArea(Shape sh) | // 参数类型是 Shape |
| 45 | { | // CallArea 开始 |
| 46 | int a; | // 用来存面积 |
| 47 | a = sh.area(); | // 按 sh 指向对象的实际类型调用 area |
| 48 | Console.WriteLine("面积: {0}", a); | // 输出面积 |
| 49 | } | // CallArea 结束 |
| 50 | } | // Caller 类结束 |
| 51 | class Tester | // 定义 Tester 类 |
| 52 | { | // Tester 类开始 |
| 53 | | |
| 54 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 55 | { | // Main 开始 |
| 56 | Caller c = new Caller(); | // 创建 Caller 对象 |
| 57 | Rectangle r = new Rectangle(10, 7); | // 长方形:宽 10、高 7 |
| 58 | Triangle t = new Triangle(10, 5); | // 三角形:宽 10、高 5 |
| 59 | c.CallArea(r); | // 输出 Rectangle 的提示和 面积: 70 |
| 60 | c.CallArea(t); | // 输出 Triangle 的提示和 面积: 25 |
| 61 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 62 | } | // Main 结束 |
| 63 | } | // Tester 类结束 |
| 64 | } | // 命名空间结束 |
11忘了写 override
把 Rectangle 的 area 前面的 override 去掉,其余不变:
| 24 | public int area () | // 漏写了 override |
编译能通过,但有一条警告:
Program.cs(24,18): warning CS0114: “Rectangle.area()”隐藏继承的成员“Shape.area()”。若要使当前成员重写该实现,请添加关键字 override。否则,添加关键字 new。
父类的面积: 面积: 0 Triangle 类的面积: 面积: 25
没写 override,Rectangle 的 area 只是隐藏了基类的同名方法(把它盖住,但不算重写),通过 Shape 类型的 sh 调用时走的仍是 Shape 的版本,面积是 0。警告里说的“添加关键字 new”,意思是写上 new 表示有意隐藏父类的方法,不是重写。所以基类写 virtual,派生类要写 override,通过基类类型调用时才会执行派生的版本。
12小结
- 多态:同一句调用,在不同的对象上做出不同的事。
- 静态多态靠重载,编译时按参数选方法;只有返回类型不同,不能重载。
- 动态多态靠抽象类和虚方法,运行时按对象的实际类型执行;抽象类不能 new,也不能是 sealed。
- 派生类重写虚方法必须写
override,漏写只是隐藏。
下一节:两个自己写的对象,能不能像数字那样直接用加号加起来?
