第 21 节 · 第三部分 面向对象

继承

派生类在冒号后面写一个基类,基类的成员它都有(private 也在,只是碰不到);new 只造一个对象,基类的构造函数先运行;类只能继承一个基类,但能实现多个接口。

时长 7:55授课计划第 8 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1什么是继承

继承就是照着一个已有的类,去定义一个新的类:已有的叫基类,新写的叫派生类。派生类不用重写基类里的成员,拿来就能用,代码能重用,写程序和改程序都更省事。

继承表达的是“属于”关系,英文叫 IS-A:狗属于哺乳动物,哺乳动物属于动物,所以狗也属于动物。

2基类和派生类

基类也常叫父类,派生类也常叫子类。写法是 class 派生类名 : 基类名,冒号后面只能写一个基类(C# 不支持类的多重继承),但可以实现好几个接口(只规定要有哪些方法的“规格表”,第 24 节细讲)。派生类会继承基类的字段、方法、属性这些成员,还能用 base 调用基类的构造函数和方法。下面的 BaseClass 里只有一个 SomeMethod 方法,DerivedClass 继承它,又加了 AnotherMethod:

C# · 第 9-18 行
9class DerivedClass : BaseClass// DerivedClass 继承 BaseClass,冒号后面写基类名
10{// DerivedClass 类开始
11 public void AnotherMethod()// 派生类自己加的方法 AnotherMethod
12 {// AnotherMethod 开始
13 // Accessing base class method
14 base.SomeMethod();// base. 表示“基类的”:调用 BaseClass 里的 SomeMethod
15
16 // Method implementation
17 }// AnotherMethod 结束
18}// DerivedClass 类结束

这两个方法体里只有注释(// Method implementation 意思是“这里写方法的实现”),能编译;加一个调用 AnotherMethod 的 Main 运行,什么也不输出。

3例子:Shape 和 Rectangle

基类 Shape 表示形状,有 width、height 两个字段和 setWidth、setHeight 两个方法;派生类 Rectangle 自己只多写了一个 getArea。

C# · 第 19-24 行
19class Rectangle: Shape// Rectangle 继承 Shape
20{// Rectangle 类开始
21 public int getArea()// Rectangle 自己加的方法:算面积
22 {// getArea 开始
23 return (width * height);// 直接用继承来的 width、height
24 }// getArea 结束
C# · 第 31-37 行
31Rectangle Rect = new Rectangle();// 只 new 了一个 Rectangle 对象
32
33Rect.setWidth(5);// setWidth 是从 Shape 继承来的:width = 5
34Rect.setHeight(7);// setHeight 也是继承来的:height = 7
35
36// 打印对象的面积
37Console.WriteLine("总面积: {0}", Rect.getArea());// getArea 返回 5 × 7 = 35
查看完整代码(41 行)
C#
1using System;// 引入 System 命名空间
2namespace InheritanceApplication// 命名空间(把类装在一起的“文件夹”,第 25 节讲)
3{// 命名空间开始
4 class Shape// 基类 Shape:表示形状
5 {// Shape 类开始
6 public void setWidth(int w)// setWidth:设置宽
7 {// setWidth 开始
8 width = w;// 把参数 w 存进字段 width
9 }// setWidth 结束
10 public void setHeight(int h)// setHeight:设置高
11 {// setHeight 开始
12 height = h;// 把参数 h 存进字段 height
13 }// setHeight 结束
14 protected int width;// 宽,protected:外面碰不到,派生类里能用
15 protected int height;// 高,同样是 protected
16 }// Shape 类结束
17
18 // 派生类
19 class Rectangle: Shape// 派生类 Rectangle,冒号后面是基类 Shape
20 {// Rectangle 类开始
21 public int getArea()// Rectangle 自己加的方法:算面积
22 {// getArea 开始
23 return (width * height);// 返回宽 × 高
24 }// getArea 结束
25 }// Rectangle 类结束
26
27 class RectangleTester// 测试用的类 RectangleTester
28 {// RectangleTester 类开始
29 static void Main(string[] args)// 程序入口 Main 方法
30 {// Main 开始
31 Rectangle Rect = new Rectangle();// 创建一个 Rectangle 对象
32
33 Rect.setWidth(5);// 调用继承来的 setWidth:width = 5
34 Rect.setHeight(7);// 调用继承来的 setHeight:height = 7
35
36 // 打印对象的面积
37 Console.WriteLine("总面积: {0}", Rect.getArea());// 输出面积:5 × 7 = 35
38 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
39 }// Main 结束
40 }// RectangleTester 类结束
41}// 命名空间结束
运行结果
总面积: 35

getArea 能直接用 width、height,是因为它们标了 protected(受保护的:类外面的代码碰不到,派生类里可以用):

C# · 第 14-15 行
14protected int width;// 宽,protected
15protected int height;// 高,protected

视频里把它比成一台叠起来的机柜:Shape 是底层,有宽、高两个读数窗和两颗旋钮;上层是 Rectangle 自己加的面积表。

4private 成员也会被继承

private(私有的)成员也会被继承,只是派生类里的代码碰不到它(教程说“private 成员不会被继承”,不准确)。给 Shape 加一个 private 的 id 和一个打印它的 ShowId,Rectangle 里什么也不写:

C# · 第 5-9 行
5private int id = 7;// private 字段 id,初值 7
6
7public void ShowId()// Shape 自己的方法,能打印 id
8{// ShowId 开始
9 Console.WriteLine("编号: {0}", id);// Shape 里面的代码可以用 id
C# · 第 21-22 行
21Rectangle r = new Rectangle();// 创建 Rectangle 对象(Rectangle 类里是空的)
22r.ShowId();// 调用继承来的 ShowId,打印出这个对象里的 id
运行结果
编号: 7

id 就在这个对象里。可是在 Rectangle 里直接写 id,编译器报错:

C# · 第 15-18 行
15public int GetId()// 在派生类 Rectangle 里加一个方法
16{// GetId 开始
17 return id;// 直接用基类的 private 字段 id
18}// GetId 结束
编译报错
Program.cs(17,16): error CS0122: “Shape.id”不可访问,因为它具有一定的保护级别

所以 id 跟着继承下来了,但只有 Shape 自己的方法(比如 ShowId)能用它。

5基类的初始化

这次基类是 Rectangle,构造函数接收长和宽,Display 打印长、宽和面积;Tabletop(桌面)继承它,多了算成本的 GetCost:面积 × 70。

C# · 第 28-29 行
28public Tabletop(double l, double w) : base(l, w)// : base(l, w) 把 l、w 交给 Rectangle 的构造函数
29{ }// Tabletop 构造函数体:空的

: base(l, w) 让基类先把自己那部分字段 length、width 设好。

C# · 第 36-40 行
36public void Display()// Tabletop 也写了一个 Display,和基类的同名
37{// Tabletop 的 Display 开始
38 base.Display();// 先调用基类的 Display:打印长、宽、面积三行
39 Console.WriteLine("成本: {0}", GetCost());// 再加一行成本:33.75 × 70 = 2362.5
40}// Tabletop 的 Display 结束
查看完整代码(51 行)
C#
1using System;// 引入 System 命名空间
2namespace RectangleApplication// 定义命名空间 RectangleApplication
3{// 命名空间开始
4 class Rectangle// 基类 Rectangle:长方形
5 {// Rectangle 类开始
6 // 成员变量
7 protected double length;// 长,protected:派生类里能用
8 protected double width;// 宽
9 public Rectangle(double l, double w)// Rectangle 的构造函数:接收长和宽
10 {// Rectangle 构造函数开始
11 length = l;// 存长:4.5
12 width = w;// 存宽:7.5
13 }// Rectangle 构造函数结束
14 public double GetArea()// GetArea:算面积
15 {// GetArea 开始
16 return length * width;// 4.5 × 7.5 = 33.75
17 }// GetArea 结束
18 public void Display()// 基类的 Display:打印三行
19 {// Rectangle 的 Display 开始
20 Console.WriteLine("长度: {0}", length);// 第 1 行:长度 4.5
21 Console.WriteLine("宽度: {0}", width);// 第 2 行:宽度 7.5
22 Console.WriteLine("面积: {0}", GetArea());// 第 3 行:面积 33.75
23 }// Rectangle 的 Display 结束
24 }//end class Rectangle// Rectangle 类结束
25 class Tabletop : Rectangle// 派生类 Tabletop(桌面)继承 Rectangle
26 {// Tabletop 类开始
27 private double cost;// 字段 cost,后面一次也没用上(警告 CS0169)
28 public Tabletop(double l, double w) : base(l, w)// Tabletop 的构造函数,长、宽交给基类构造函数
29 { }// Tabletop 构造函数体:空的
30 public double GetCost()// GetCost:算成本
31 {// GetCost 开始
32 double cost;// 局部变量 cost,和上面的字段同名但不是同一个
33 cost = GetArea() * 70;// 调用继承来的 GetArea:33.75 × 70 = 2362.5
34 return cost;// 返回成本
35 }// GetCost 结束
36 public void Display()// Tabletop 的 Display,和基类的同名(警告 CS0108)
37 {// Tabletop 的 Display 开始
38 base.Display();// 先调用基类的 Display,打印三行
39 Console.WriteLine("成本: {0}", GetCost());// 第 4 行:成本 2362.5
40 }// Tabletop 的 Display 结束
41 }// Tabletop 类结束
42 class ExecuteRectangle// 测试用的类 ExecuteRectangle
43 {// ExecuteRectangle 类开始
44 static void Main(string[] args)// 程序入口 Main 方法
45 {// Main 开始
46 Tabletop t = new Tabletop(4.5, 7.5);// 创建一张 4.5 × 7.5 的桌面
47 t.Display();// 调用 Tabletop 的 Display,输出四行
48 Console.ReadLine();// 等待按回车(现在的 VS 不写也会停住)
49 }// Main 结束
50 }// ExecuteRectangle 类结束
51}// 命名空间结束
运行结果
长度: 4.5
宽度: 7.5
面积: 33.75
成本: 2362.5

编译时还有两个警告:Tabletop 的 Display 和基类的同名,编译器建议加上 new 表示有意隐藏(用自己的同名方法盖住基类的,第 22 节会讲);字段 cost 一次也没用上。

编译警告
Program.cs(36,19): warning CS0108: “Tabletop.Display()”隐藏继承的成员“Rectangle.Display()”。如果是有意隐藏,请使用关键字 new。
Program.cs(27,22): warning CS0169: 从不使用字段“Tabletop.cost”

只造一个对象,基类构造函数先运行

new 只造出一个对象,基类的构造函数先运行,派生类的构造函数后运行(教程说“父类对象要在子类对象之前被创建”,容易误会成两个对象)。在两个构造函数里各打印一行:

C# · 第 12 行
12Console.WriteLine("1. Rectangle 的构造函数,对象是 {0}", GetType().Name);// Rectangle 构造函数里打印;GetType().Name 取对象的类型名
C# · 第 18-20 行
18public Tabletop(double l, double w) : base(l, w)// 先跳去运行 Rectangle 的构造函数
19{// Tabletop 构造函数开始
20 Console.WriteLine("2. Tabletop 的构造函数");// 基类那部分设好之后才运行这一行
运行结果
1. Rectangle 的构造函数,对象是 Tabletop
2. Tabletop 的构造函数

Rectangle 的构造函数先运行,而且它看到的对象类型已经是 Tabletop:基类和派生类的字段都排在同一个对象里。C# 里给基类构造函数传参数,就写构造函数后面的 : base(...)(教程叫它“成员初始化列表”,那是 C++ 的叫法)。

去掉 : base 会怎样

C# · 第 17-19 行
17public Tabletop(double l, double w)// 删掉了 : base(l, w)
18{// Tabletop 构造函数开始
19}// Tabletop 构造函数结束
编译报错
Program.cs(17,12): error CS7036: 未提供与“Rectangle.Rectangle(double, double)”的所需参数“l”对应的参数

不写 : base(...) 时,编译器会去调用 Rectangle 不带参数的构造函数;可 Rectangle 只有带 l、w 的那个,所以报“没给参数 l 传值”。

6继承接口

这里的接口只规定要有哪些方法,不写方法怎么做。一个接口可以继承一个或多个别的接口,派生接口会拿到基接口的全部成员,可以往里加,但不能改变它们的访问修饰符(public、private 这类写在前面、决定谁能用的词)。

C# · 第 4-13 行
4interface IBaseInterface// 基接口 IBaseInterface
5{// IBaseInterface 开始
6 void Method1();// 只规定要有 Method1,没有方法体
7}// IBaseInterface 结束
8
9// 定义一个派生接口,继承自基接口
10interface IDerivedInterface : IBaseInterface// 派生接口:冒号后面是它继承的接口
11{// IDerivedInterface 开始
12 void Method2();// 自己又加了 Method2;Method1 是继承来的
13}// IDerivedInterface 结束

MyClass 实现了 IDerivedInterface,就得把 Method1 和 Method2 都写出来:

查看完整代码(42 行)
C#
1using System;// 引入 System 命名空间
2
3// 定义一个基接口
4interface IBaseInterface// 基接口 IBaseInterface
5{// IBaseInterface 开始
6 void Method1();// 规定要有 Method1
7}// IBaseInterface 结束
8
9// 定义一个派生接口,继承自基接口
10interface IDerivedInterface : IBaseInterface// 派生接口 IDerivedInterface 继承 IBaseInterface
11{// IDerivedInterface 开始
12 void Method2();// 自己加的 Method2
13}// IDerivedInterface 结束
14
15// 实现派生接口的类
16class MyClass : IDerivedInterface// MyClass 实现 IDerivedInterface
17{// MyClass 类开始
18 public void Method1()// 基接口规定的 Method1,也必须写
19 {// Method1 开始
20 Console.WriteLine("Method1 implementation");// 输出一行
21 }// Method1 结束
22
23 public void Method2()// 派生接口规定的 Method2
24 {// Method2 开始
25 Console.WriteLine("Method2 implementation");// 输出一行
26 }// Method2 结束
27}// MyClass 类结束
28
29class Program// 定义 Program 类
30{// Program 类开始
31 static void Main(string[] args)// 程序入口 Main 方法
32 {// Main 开始
33 // 创建 MyClass 类的实例
34 MyClass obj = new MyClass();// 创建 MyClass 对象
35
36 // 调用继承自基接口的方法
37 obj.Method1();// 输出 Method1 implementation
38
39 // 调用派生接口新增的方法
40 obj.Method2();// 输出 Method2 implementation
41 }// Main 结束
42}// Program 类结束
运行结果
Method1 implementation
Method2 implementation

漏写从基接口继承来的 Method1,编译器报错:

C# · 第 13-18 行
13class MyClass : IDerivedInterface// MyClass 实现 IDerivedInterface
14{// MyClass 类开始
15 public void Method2()// 只写了 Method2,漏了 Method1
16 {// Method2 开始
17 Console.WriteLine("Method2 implementation");// 输出一行
18 }// Method2 结束
编译报错
Program.cs(13,17): error CS0535: “MyClass”不实现接口成员“IBaseInterface.Method1()”

7多重继承

多重继承是说一个类型同时从几个父类那里继承特征和行为;只能有一个父类的叫单一继承。C# 不支持类的多重继承,冒号后面写两个类名就报错:

C# · 第 11 行
11class Rectangle : Shape, PaintCost// Shape 和 PaintCost 在这里都是类
编译报错
Program.cs(11,26): error CS1721: 类“Rectangle”不能具有多个基类:“Shape”和“PaintCost”

不过可以借助接口:Rectangle 继承 Shape,同时实现 PaintCost 这个接口。PaintCost 是接口(代码第 18 行注释写的“基类”不对),冒号后面的类只有 Shape 一个。

C# · 第 19-21 行
19public interface PaintCost// PaintCost 是接口,不是基类
20{// PaintCost 接口开始
21 int getCost(int area);// 只规定要有 getCost,没带写好的代码
C# · 第 25 行
25class Rectangle : Shape, PaintCost// 一个基类 Shape + 一个接口 PaintCost
C# · 第 31-34 行
31public int getCost(int area)// getCost 还是 Rectangle 自己写的
32{// getCost 开始
33 return area * 70;// 面积 × 70:35 × 70 = 2450
34}// getCost 结束
查看完整代码(51 行)
C#
1using System;// 引入 System 命名空间
2namespace InheritanceApplication// 定义命名空间 InheritanceApplication
3{// 命名空间开始
4 class Shape// 基类 Shape
5 {// Shape 类开始
6 public void setWidth(int w)// setWidth:设置宽
7 {// setWidth 开始
8 width = w;// 把参数 w 存进 width
9 }// setWidth 结束
10 public void setHeight(int h)// setHeight:设置高
11 {// setHeight 开始
12 height = h;// 把参数 h 存进 height
13 }// setHeight 结束
14 protected int width;// 宽,protected
15 protected int height;// 高,protected
16 }// Shape 类结束
17
18 // 基类 PaintCost
19 public interface PaintCost// 上一行注释写“基类”,其实 PaintCost 是接口
20 {// PaintCost 接口开始
21 int getCost(int area);// 规定要有 getCost:接收面积,返回成本
22
23 }// PaintCost 接口结束
24 // 派生类
25 class Rectangle : Shape, PaintCost// Rectangle 继承 Shape,同时实现接口 PaintCost
26 {// Rectangle 类开始
27 public int getArea()// getArea:算面积
28 {// getArea 开始
29 return (width * height);// 返回宽 × 高
30 }// getArea 结束
31 public int getCost(int area)// 实现接口规定的 getCost
32 {// getCost 开始
33 return area * 70;// 返回面积 × 70
34 }// getCost 结束
35 }// Rectangle 类结束
36 class RectangleTester// 测试用的类 RectangleTester
37 {// RectangleTester 类开始
38 static void Main(string[] args)// 程序入口 Main 方法
39 {// Main 开始
40 Rectangle Rect = new Rectangle();// 创建 Rectangle 对象
41 int area;// 声明变量 area 存面积
42 Rect.setWidth(5);// width = 5
43 Rect.setHeight(7);// height = 7
44 area = Rect.getArea();// area = 5 × 7 = 35
45 // 打印对象的面积
46 Console.WriteLine("总面积: {0}", Rect.getArea());// 输出总面积 35
47 Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area));// 输出成本 35 × 70 = 2450,$ 是普通字符
48 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
49 }// Main 结束
50 }// RectangleTester 类结束
51}// 命名空间结束
运行结果
总面积: 35
油漆总成本: $2450

8小结

  • 冒号后面只写一个基类,基类的成员派生类里都有,private 的也在,只是碰不到。
  • new 只造一个对象,基类的构造函数先运行;要传参数就用 : base(...)。
  • 接口能继承接口,一个类也能实现好几个接口;接口只规定方法,代码还得自己写。

下一节:同一句方法调用,交给不同的对象,能不能做出不同的动作?

检查题

5 道,选完马上告诉你对不对。

  1. 1class Rectangle : Shape, PaintCost,其中 Shape 和 PaintCost 都是类,会怎样?

  2. 2Shape 里有 private int id = 7; 和一个打印它的 public 方法 ShowId,Rectangle 继承 Shape。下面哪句对?

  3. 3new Tabletop(4.5, 7.5)(Tabletop : Rectangle)时,下面哪句对?

  4. 4Tabletop 的 Display 先调用 base.Display() 再打印成本,教程程序输出几行?

  5. 5MyClass 实现 IDerivedInterface(它继承 IBaseInterface 的 Method1,又加了 Method2),只写了 Method2,会怎样?