1什么是继承
继承就是照着一个已有的类,去定义一个新的类:已有的叫基类,新写的叫派生类。派生类不用重写基类里的成员,拿来就能用,代码能重用,写程序和改程序都更省事。
继承表达的是“属于”关系,英文叫 IS-A:狗属于哺乳动物,哺乳动物属于动物,所以狗也属于动物。
2基类和派生类
基类也常叫父类,派生类也常叫子类。写法是 class 派生类名 : 基类名,冒号后面只能写一个基类(C# 不支持类的多重继承),但可以实现好几个接口(只规定要有哪些方法的“规格表”,第 24 节细讲)。派生类会继承基类的字段、方法、属性这些成员,还能用 base 调用基类的构造函数和方法。下面的 BaseClass 里只有一个 SomeMethod 方法,DerivedClass 继承它,又加了 AnotherMethod:
| 9 | class 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。
| 19 | class Rectangle: Shape | // Rectangle 继承 Shape |
| 20 | { | // Rectangle 类开始 |
| 21 | public int getArea() | // Rectangle 自己加的方法:算面积 |
| 22 | { | // getArea 开始 |
| 23 | return (width * height); | // 直接用继承来的 width、height |
| 24 | } | // getArea 结束 |
| 31 | Rectangle Rect = new Rectangle(); | // 只 new 了一个 Rectangle 对象 |
| 32 | | |
| 33 | Rect.setWidth(5); | // setWidth 是从 Shape 继承来的:width = 5 |
| 34 | Rect.setHeight(7); | // setHeight 也是继承来的:height = 7 |
| 35 | | |
| 36 | // 打印对象的面积 | |
| 37 | Console.WriteLine("总面积: {0}", Rect.getArea()); | // getArea 返回 5 × 7 = 35 |
查看完整代码(41 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace 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(受保护的:类外面的代码碰不到,派生类里可以用):
| 14 | protected int width; | // 宽,protected |
| 15 | protected int height; | // 高,protected |
视频里把它比成一台叠起来的机柜:Shape 是底层,有宽、高两个读数窗和两颗旋钮;上层是 Rectangle 自己加的面积表。
4private 成员也会被继承
private(私有的)成员也会被继承,只是派生类里的代码碰不到它(教程说“private 成员不会被继承”,不准确)。给 Shape 加一个 private 的 id 和一个打印它的 ShowId,Rectangle 里什么也不写:
| 5 | private int id = 7; | // private 字段 id,初值 7 |
| 6 | | |
| 7 | public void ShowId() | // Shape 自己的方法,能打印 id |
| 8 | { | // ShowId 开始 |
| 9 | Console.WriteLine("编号: {0}", id); | // Shape 里面的代码可以用 id |
| 21 | Rectangle r = new Rectangle(); | // 创建 Rectangle 对象(Rectangle 类里是空的) |
| 22 | r.ShowId(); | // 调用继承来的 ShowId,打印出这个对象里的 id |
编号: 7
id 就在这个对象里。可是在 Rectangle 里直接写 id,编译器报错:
| 15 | public 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。
| 28 | public Tabletop(double l, double w) : base(l, w) | // : base(l, w) 把 l、w 交给 Rectangle 的构造函数 |
| 29 | { } | // Tabletop 构造函数体:空的 |
: base( 让基类先把自己那部分字段 length、width 设好。
| 36 | public 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 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace 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 只造出一个对象,基类的构造函数先运行,派生类的构造函数后运行(教程说“父类对象要在子类对象之前被创建”,容易误会成两个对象)。在两个构造函数里各打印一行:
| 12 | Console.WriteLine("1. Rectangle 的构造函数,对象是 {0}", GetType().Name); | // Rectangle 构造函数里打印;GetType().Name 取对象的类型名 |
| 18 | public 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 会怎样
| 17 | public 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 这类写在前面、决定谁能用的词)。
| 4 | interface IBaseInterface | // 基接口 IBaseInterface |
| 5 | { | // IBaseInterface 开始 |
| 6 | void Method1(); | // 只规定要有 Method1,没有方法体 |
| 7 | } | // IBaseInterface 结束 |
| 8 | | |
| 9 | // 定义一个派生接口,继承自基接口 | |
| 10 | interface IDerivedInterface : IBaseInterface | // 派生接口:冒号后面是它继承的接口 |
| 11 | { | // IDerivedInterface 开始 |
| 12 | void Method2(); | // 自己又加了 Method2;Method1 是继承来的 |
| 13 | } | // IDerivedInterface 结束 |
MyClass 实现了 IDerivedInterface,就得把 Method1 和 Method2 都写出来:
查看完整代码(42 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | // 定义一个基接口 | |
| 4 | interface IBaseInterface | // 基接口 IBaseInterface |
| 5 | { | // IBaseInterface 开始 |
| 6 | void Method1(); | // 规定要有 Method1 |
| 7 | } | // IBaseInterface 结束 |
| 8 | | |
| 9 | // 定义一个派生接口,继承自基接口 | |
| 10 | interface IDerivedInterface : IBaseInterface | // 派生接口 IDerivedInterface 继承 IBaseInterface |
| 11 | { | // IDerivedInterface 开始 |
| 12 | void Method2(); | // 自己加的 Method2 |
| 13 | } | // IDerivedInterface 结束 |
| 14 | | |
| 15 | // 实现派生接口的类 | |
| 16 | class 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 | | |
| 29 | class 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,编译器报错:
| 13 | class 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# 不支持类的多重继承,冒号后面写两个类名就报错:
| 11 | class Rectangle : Shape, PaintCost | // Shape 和 PaintCost 在这里都是类 |
Program.cs(11,26): error CS1721: 类“Rectangle”不能具有多个基类:“Shape”和“PaintCost”
不过可以借助接口:Rectangle 继承 Shape,同时实现 PaintCost 这个接口。PaintCost 是接口(代码第 18 行注释写的“基类”不对),冒号后面的类只有 Shape 一个。
| 19 | public interface PaintCost | // PaintCost 是接口,不是基类 |
| 20 | { | // PaintCost 接口开始 |
| 21 | int getCost(int area); | // 只规定要有 getCost,没带写好的代码 |
| 25 | class Rectangle : Shape, PaintCost | // 一个基类 Shape + 一个接口 PaintCost |
| 31 | public int getCost(int area) | // getCost 还是 Rectangle 自己写的 |
| 32 | { | // getCost 开始 |
| 33 | return area * 70; | // 面积 × 70:35 × 70 = 2450 |
| 34 | } | // getCost 结束 |
查看完整代码(51 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace 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(...)。- 接口能继承接口,一个类也能实现好几个接口;接口只规定方法,代码还得自己写。
下一节:同一句方法调用,交给不同的对象,能不能做出不同的动作?
