第十一单元 面向对象三:继承与多态

打印 上一主题 下一主题

主题 679|帖子 679|积分 2037

假设老师类设计如下:
  1. class 老师类
  2. {
  3.     属性:姓名,性别,生日,工资
  4.     行为:吃饭,跑步,教学
  5. }
复制代码
学生类设计如下:
  1. class 老师类
  2. {
  3.     属性:姓名,性别,生日,班级
  4.     行为:吃饭,跑步,学习
  5. }
复制代码
  
我们秉承着,让最简洁的代码,实现最最强大的功能原则,能否让上述案例中的重复代码进行优化呢?我们能否将学生类与老师类再进行抽象,得到一个人类?这章节学习继承与多态。
1. 继承

继承是面向对象程序设计中最重要的概念之一。继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得更容易。同时也有利于重用代码和节省开发时间。
当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类
继承的思想实现了 属于(IS-A) 关系。例如,哺乳动物 属于(IS-A) 动物,狗 属于(IS-A) 哺乳动物,因此狗 属于(IS-A) 动物。
基类和派生类

一个类可以派生自多个类或接口,这意味着它可以从多个基类或接口继承数据和函数。
C# 中创建派生类的语法如下:
  1. <访问修饰符> class <基类>
  2. {
  3. ...
  4. }
  5. class <派生类> : <基类>
  6. {
  7. ...
  8. }
复制代码
  
现在我们将上述学生类,老师类案例使用继承,进行代码优化:
  1. // 把公共的属性与方法提取出来,封装成父类
  2. public class Person
  3. {
  4.     public string Name { get; set; }
  5.     public string Sex { get; set; }
  6.     public DateTime Birthday { get; set; }
  7.    
  8.     public void Eat()
  9.     {
  10.         ....
  11.     }
  12.    
  13.     public void Run()
  14.     {
  15.         
  16.     }
  17. }
  18. // 老师类设计如下:
  19. public class Teacher : Person // 继承Person
  20. {
  21.     public int Salary { get; set;}
  22.    
  23.     // 教学方法
  24.     public void Teach()
  25.     {
  26.         ...
  27.     }
  28. }
  29. // 学生类设计如下
  30. public class Student : Person
  31. {
  32.     public string ClassName { get; set; }
  33.    
  34.     // 学习方法
  35.     public void Study()
  36.     {
  37.         ...
  38.     }
  39. }
复制代码
 
基类的初始化

派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。
下面的程序演示了这点:
  1. class Rectangle
  2. {
  3.     // 成员变量
  4.     protected double length;
  5.     protected double width;
  6.     public Rectangle(double l, double w)
  7.     {
  8.         length = l;
  9.         width = w;
  10.     }
  11.     public double GetArea()
  12.     {
  13.         return length * width;
  14.     }
  15.     public void Display()
  16.     {
  17.         Console.WriteLine("长度: {0}", length);
  18.         Console.WriteLine("宽度: {0}", width);
  19.         Console.WriteLine("面积: {0}", GetArea());
  20.     }
  21. }//end class Rectangle  
  22. class Tabletop : Rectangle
  23. {
  24.     private double cost;
  25.     public Tabletop(double l, double w) : base(l, w)
  26.     { }
  27.     public double GetCost()
  28.     {
  29.         double cost;
  30.         cost = GetArea() * 70;
  31.         return cost;
  32.     }
  33.     public void Display()
  34.     {
  35.         base.Display();
  36.         Console.WriteLine("成本: {0}", GetCost());
  37.     }
  38. }
  39. class Program
  40. {
  41.     static void Main(string[] args)
  42.     {
  43.         Tabletop t = new Tabletop(4.5, 7.5);
  44.         t.Display();
  45.         Console.ReadLine();
  46.     }
  47. }
复制代码
当上面的代码被编译和执行时,它会产生下列结果:
  1. 长度: 4.5
  2. 宽度: 7.5
  3. 面积: 33.75
  4. 成本: 2362.5
复制代码
注意


  • C# 中 不支持多继承基类:一个类只能继承一个直接父类。
  • 继承需要满足什么样的设计规范?

    • 子类们相同特征(共性属性,共性方法)放在父类中定义。
    • 子类独有的的属性和行为应该定义在子类自己里面。

  • 所有的类都是Object类的子类。
  • 子类可以继承父类的属性和行为(除静态属性和静态方法外),但是子类不能继承父类的构造器(如果非要继承,需要额外的写代码)。
2. 方法重写

什么是重写?
“重写”父类方法就是修改它的实现方式或者说在子类中对它进行重新编写。
为什么要重写父类的方法
通常,子类继承父类的方法,在调用对象继承方法的时候,调用和执行的是父类的实现。但是,有时候需要对子类中的继承方法有不同的实现方式。例如,假设动物存在“跑”的方法,从中继承有狗类和马类两个子类,狗与马的奔跑速度或者动作都不太一样。
如何重写

  • 重写父类的方法要用到override关键字(具有override关键字修饰的方法是对父类中同名方法的新实现)
  • 要重写父类的方法,前提是父类中该要被重写的方法必须声明为virtual或者是abstract类型。给父类中要被重写的方法添加virtual关键字表示可以在子类中重写它的实现。(注意:C#中的方法默认并不是virtual类型的因此要添加virtual关键字才能够被重写)
  • virtual关键字用于将方法定义为支持多态,有virtual关键字修饰的方法称为“虚拟方法”
声明虚方法
  1. [访问修饰符] virtual [返回类型] 方法名(参数列表)
  2. {
  3.         //虚拟方法的实现,该方法可以被子类重写
  4. }
复制代码
 
  1. class Employee
  2. {
  3.     public virtual void EmpInfo()
  4.     {
  5.         Console.WriteLine("用virtual关键字修饰的方法是虚拟方法");
  6.     }
  7. }
  8. class DervEmployee : Employee
  9. {
  10.     public override void EmpInfo()
  11.     {
  12.         base.EmpInfo();//base关键字将在下面拓展中提到
  13.         Console.WriteLine("该方法重写base方法");
  14.     }
  15. }
  16. class Program
  17. {
  18.     static void Main(string[] args)
  19.     {
  20.         DervEmployee objDervEmployee = new DervEmployee();
  21.         objDervEmployee.EmpInfo();
  22.         //注意:objDervEmployee派生类的实例是赋给Employee类的objEmployee的引用,
  23.         // 所以objEmployee引用调用EmpInfo()方法时 还是调用DervEmployee类的方法
  24.         Employee objEmployee = objDervEmployee;
  25.         objEmployee.EmpInfo();
  26.     }
  27. }
复制代码
 
base关键字用于从子类中访问父类成员。即使父类的方法在子类中重写,仍可以使用base关键字调用。
而且,在创建子类实例时,可以使用base关键字调用父类的构造函数。使用base关键字只能访问父类的构造函数、实例方法或实例属性,而不能访问基类的静态方法。
隐藏父类方法

如果父类与子类具有相同的方法,然后父方法并没有加virtual 关键字修饰。此时,子类需要用 new 关键字进行隐藏父类的方法.
  1. [/code] 
  2. [size=5]3. 抽象类[/size]
  3. 使用 [url=https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/abstract]abstract[/url] 关键字可以创建不完整且必须在派生类中实现的类和 [url=https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/class]class[/url] 成员。
  4. [code]public abstract class A
  5. {
  6.     // Class members here.
  7. }
复制代码
 
抽象类不能实例化。 抽象类的用途是提供一个可供多个派生类共享的通用基类定义。 例如,类库可以定义一个抽象类,将其用作多个类库函数的参数,并要求使用该库的程序员通过创建派生类来提供自己的类实现。
抽象类也可以定义抽象方法。 方法是将关键字 abstract 添加到方法的返回类型的前面。 例如:
  1. public abstract class A
  2. {
  3.     public abstract void DoWork(int i);
  4. }
复制代码
 
抽象方法没有实现,所以方法定义后面是分号,而不是常规的方法块。 抽象类的派生类必须实现所有抽象方法。 当抽象类从基类继承虚方法时,抽象类可以使用抽象方法重写该虚方法。 例如:
  1. public class D
  2. {
  3.     public virtual void DoWork(int i)
  4.     {
  5.         // Original implementation.
  6.     }
  7. }
  8. public abstract class E : D
  9. {
  10.     public abstract override void DoWork(int i);
  11. }
  12. public class F : E
  13. {
  14.     public override void DoWork(int i)
  15.     {
  16.         // New implementation.
  17.     }
  18. }
复制代码
 
如果将 virtual 方法声明为 abstract,则该方法对于从抽象类继承的所有类而言仍然是虚方法。 继承抽象方法的类无法访问方法的原始实现,因此在上一示例中,类 F 上的 DoWork 无法调用类 D 上的 DoWork。通过这种方式,抽象类可强制派生类向虚拟方法提供新的方法实现。
4. 密封类

使用 sealed 关键字可以防止继承以前标记为 virtual 的类或某些类成员。
  1. public sealed class D
  2. {
  3.     // Class members here.
  4. }
复制代码
  
密封类不能用作基类。 因此,它也不能是抽象类。 密封类禁止派生。 由于密封类从不用作基类,所以有些运行时优化可以略微提高密封类成员的调用速度。
在对基类的虚成员进行重写的派生类上,方法、索引器、属性或事件可以将该成员声明为密封成员。 在用于以后的派生类时,这将取消成员的虚效果。 方法是在类成员声明中将 sealed 关键字置于 sealed 关键字前面。 例如:
  1. public class D : C
  2. {
  3.     public sealed override void DoWork() { }
  4. }
复制代码
 
5. 接口

接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 "是什么" 部分,派生类定义了语法合同 "怎么做" 部分。
接口定义了属性、方法和事件,这些都是接口的成员。接口只包含了成员的声明(c#8.0以外,接口也可以有默认实现)。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。
接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。
接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。
抽象类不能直接实例化,但允许派生出具体的,具有实际功能的类。
 
高内聚,低耦合:尽量依赖于接口,不依赖于实现,正所谓面向接口编程。说白了,就是为了解耦。
定义接口

 
接口使用 interface 关键字声明,它与类的声明类似。接口声明默认是 public 的。下面是一个接口声明的实例:
  1. interface IMyInterface
  2. {
  3.     void MethodToImplement();
  4. }
复制代码
  
以上代码定义了接口 IMyInterface。通常接口命令以 I 字母开头,这个接口只有一个方MethodToImplement(),没有参数和返回值,当然我们可以按照需求设置参数和返回值。值得注意的是,该方法并没有具体的实现。
实现接口
  1. class InterfaceImplementer : IMyInterface
  2. {
  3.     public void MethodToImplement()
  4.     {
  5.         Console.WriteLine("MethodToImplement() called.");
  6.     }
  7. }
  8. class Program
  9. {
  10.      static void Main()
  11.     {
  12.         InterfaceImplementer iImp = new InterfaceImplementer();
  13.         iImp.MethodToImplement();
  14.     }
  15. }
复制代码
 
InterfaceImplementer类实现了 IMyInterface 接口,接口的实现与类的继承语法格式类似:
  1. class InterfaceImplementer : IMyInterface
复制代码
继承接口后,我们需要实现接口的方法 MethodToImplement(), 方法名必须与接口定义的方法名一致。
接口继承

以下实例定义了两个接口 IMyInterface 和 IParentInterface。
如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。
以下实例IMyInterface 继承了 IParentInterface 接口,因此接口实现类必须实现 MethodToImplement()和 ParentInterfaceMethod()方法:
  1. interface IParentInterface
  2. {
  3.     void ParentInterfaceMethod();
  4. }
  5. interface IMyInterface : IParentInterface
  6. {
  7.     void MethodToImplement();
  8. }
  9. class InterfaceImplementer : IMyInterface
  10. {
  11.     public void MethodToImplement()
  12.     {
  13.         Console.WriteLine("MethodToImplement() called.");
  14.     }
  15.     public void ParentInterfaceMethod()
  16.     {
  17.         Console.WriteLine("ParentInterfaceMethod() called.");
  18.     }
  19. }
  20. class Program
  21. {
  22.     static void Main()
  23.     {
  24.         InterfaceImplementer iImp = new InterfaceImplementer();
  25.         iImp.MethodToImplement();
  26.         iImp.ParentInterfaceMethod();
  27.     }
  28. }
复制代码
 
显式接口实现

如果一个实现的两个接口包含签名相同的成员,则在该类上实现此成员会导致这两个接口将此成员用作其实现。 如下示例中,所有对 Paint 的调用皆调用同一方法。 第一个示例定义类型:
  1. public interface IControl
  2. {
  3.     void Paint();
  4. }
  5. public interface ISurface
  6. {
  7.     void Paint();
  8. }
  9. public class SampleClass : IControl, ISurface
  10. {
  11.     // Both ISurface.Paint and IControl.Paint call this method.
  12.     public void Paint()
  13.     {
  14.         Console.WriteLine("Paint method in SampleClass");
  15.     }
  16. }
  17. class Program
  18. {
  19.     static void Main(string[] args)
  20.     {
  21.         SampleClass sample = new SampleClass();
  22.         IControl control = sample;
  23.         ISurface surface = sample;
  24.         // The following lines all call the same method.
  25.         sample.Paint();
  26.         control.Paint();
  27.         surface.Paint();
  28.     }
  29. }
复制代码
输入结果如下:
  1. Paint method in SampleClass
  2. Paint method in SampleClass
  3. Paint method in SampleClass
复制代码
但你可能不希望为这两个接口都调用相同的实现。 若要调用不同的实现,根据所使用的接口,可以显式实现接口成员。 显式接口实现是一个类成员,只通过指定接口进行调用。 通过在类成员前面加上接口名称和句点可命名该类成员。 例如:
  1. public class SampleClass : IControl, ISurface
  2. {
  3.     void IControl.Paint()
  4.     {
  5.         System.Console.WriteLine("IControl.Paint");
  6.     }
  7.     void ISurface.Paint()
  8.     {
  9.         System.Console.WriteLine("ISurface.Paint");
  10.     }
  11. }
  12. class Program
  13. {
  14.     static void Main(string[] args)
  15.     {
  16.         SampleClass sample = new SampleClass();
  17.         IControl control = sample;
  18.         ISurface surface = sample;
  19.       
  20.         // sample.Paint(); // 此行报错,因为显示接口实现的方法不能直接通过类调用
  21.         control.Paint();
  22.         surface.Paint(); // 显示接口实现,只能通过接口调用方法
  23.     }
  24. }
复制代码
输入结果:
  1. IControl.Paint
  2. ISurface.Paint
复制代码
  
C# 8.0 开始,你可以为在接口中声明的成员定义一个实现。 如果类从接口继承方法实现,则只能通过接口类型的引用访问该方法。 继承的成员不会显示为公共接口的一部分。 下面的示例定义接口方法的默认实现:
  1. interface IAnimal
  2. {
  3.     void Roar()
  4.     {
  5.         Console.WriteLine("动物在叫");
  6.     }
  7. }
  8. class Tigger : IAnimal
  9. {
  10.     // 因为Roar()已经有默认实现,则可以不必强制实现,如果实现了,则接口默认实现失效
  11.     //public void Roar()
  12.     //{
  13.     //    Console.WriteLine("老虎在吼");
  14.     //}
  15. }
  16. class Program
  17. {
  18.     static void Main(string[] args)
  19.     {
  20.         IAnimal animal = new Tigger();
  21.         animal.Roar();
  22.     }
  23. }
复制代码
6. 多态

多态是同一个行为具有多个不同表现形式或形态的能力。
多态性意味着有多重形式。在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。
多态性可以是静态的或动态的。在静态多态性中,函数的响应是在编译时发生的。在动态多态性中,函数的响应是在运行时发生的。
在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。
多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:

现实中,比如我们按下 F1 键这个动作:

  • 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
  • 如果当前在 Word 下弹出的就是 Word 帮助;
  • 在 Windows 下弹出的就是 Windows 帮助和支持。
同一个事件发生在不同的对象上会产生不同的结果。
静态多态性

在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。C# 提供了两种技术来实现静态多态性。分别为:

  • 方法重载
  • 运算符重载
运算符重载 基本上用不上,本教案中不给予讲解。
方法重载

您可以在同一个范围内对相同的函数名有多个定义。函数的定义必须彼此不同,可以是参数列表中的参数类型不同,也可以是参数个数不同。不能重载只有返回类型不同的函数声明。
下面的实例演示了几个相同的函数 Add(),用于对不同个数参数进行相加处理:
  1. public class MyMath  
  2. {  
  3.     public int Add(int a, int b, int c)  
  4.     {  
  5.         return a + b + c;  
  6.     }  
  7.     public int Add(int a, int b)  
  8.     {  
  9.         return a + b;  
  10.     }  
  11. }  
  12. class Program  
  13. {  
  14.     static void Main(string[] args)  
  15.     {  
  16.         MyMath dataClass = new MyMath();
  17.         int add1 = dataClass.Add(1, 2);  
  18.         int add2 = dataClass.Add(1, 2, 3);
  19.         Console.WriteLine("add1 :" + add1);
  20.         Console.WriteLine("add2 :" + add2);  
  21.     }  
  22. }  
复制代码
 
下面的实例演示了几个相同的函数 print(),用于打印不同的数据类型:
  1. class Printdata
  2. {
  3.     void print(int i)
  4.     {
  5.         Console.WriteLine("输出整型: {0}", i );
  6.     }
  7.     void print(double f)
  8.     {
  9.         Console.WriteLine("输出浮点型: {0}" , f);
  10.     }
  11.     void print(string s)
  12.     {
  13.         Console.WriteLine("输出字符串: {0}", s);
  14.     }
  15. }
  16. class Program
  17. {
  18.     static void Main(string[] args)
  19.     {
  20.         Printdata p = new Printdata();
  21.         // 调用 print 来打印整数
  22.         p.print(1);
  23.         // 调用 print 来打印浮点数
  24.         p.print(1.23);
  25.         // 调用 print 来打印字符串
  26.         p.print("Hello Runoob");
  27.         Console.ReadKey();
  28.     }
  29. }
复制代码
 
当上面的代码被编译和执行时,它会产生下列结果:
  1. 输出整型: 1
  2. 输出浮点型: 1.23
  3. 输出字符串: Hello Runoob
复制代码
  
动态多态性

动态多态性是通过 抽象类 / 接口虚方法 实现的。
语法:
  1. 父类类型 对象名称 = new 子类构造器;
  2. 接口    对象名称 = new 实现类构造器;
复制代码
多态中成员访问特点

  • 方法调用:编译看左边,运行看右边。
  • 变量调用:编译看左边,运行也看左边。(多态侧重行为多态)
多态的前提

  • 有继承/实现关系;有父类引用指向子类对象;有方法重写。
优势

  • 在多态形式下,右边对象可以实现解耦合,便于扩展和维护。
  1. Animal a = new Dog();
  2. a.run(); // 后续业务行为随对象而变,后续代码无需修改
复制代码

  • 定义方法的时候,使用父类型作为参数,该方法就可以接收这父类的一切子类对象,体现出多态的扩展性与便利。
多态下会产生的一个问题:

  • 多态下不能使用子类的独有功能
以下实例创建了 Shape 基类,并创建派生类 Circle、 Rectangle、Triangle, Shape 类提供一个名为 Draw 的虚拟方法,在每个派生类中重写该方法以绘制该类的指定形状。
  1. public class Shape
  2. {
  3.     public int X { get; private set; }
  4.     public int Y { get; private set; }
  5.     public int Height { get; set; }
  6.     public int Width { get; set; }
  7.    
  8.     // 虚方法
  9.     public virtual void Draw()
  10.     {
  11.         Console.WriteLine("执行基类的画图任务");
  12.     }
  13. }
  14. class Circle : Shape
  15. {
  16.     public override void Draw()
  17.     {
  18.         Console.WriteLine("画一个圆形");
  19.         base.Draw();
  20.     }
  21. }
  22. class Rectangle : Shape
  23. {
  24.     public override void Draw()
  25.     {
  26.         Console.WriteLine("画一个长方形");
  27.         base.Draw();
  28.     }
  29. }
  30. class Triangle : Shape
  31. {
  32.     public override void Draw()
  33.     {
  34.         Console.WriteLine("画一个三角形");
  35.         base.Draw();
  36.     }
  37. }
  38. class Program
  39. {
  40.     static void Main(string[] args)
  41.     {
  42.         // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle
  43.         var shapes = new List<Shape>
  44.         {
  45.             new Rectangle(),
  46.             new Triangle(),
  47.             new Circle()
  48.         };
  49.         // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法
  50.         foreach (var shape in shapes)
  51.         {
  52.             shape.Draw();
  53.         }
  54.         Console.WriteLine("按下任意键退出。");
  55.         Console.ReadKey();
  56.     }
  57. }
复制代码
当上面的代码被编译和执行时,它会产生下列结果:
  1. 画一个长方形
  2. 执行基类的画图任务
  3. 画一个三角形
  4. 执行基类的画图任务
  5. 画一个圆形
  6. 执行基类的画图任务
  7. 按下任意键退出。
复制代码
7. 作业


  • 定义一个接口或者抽象类 MyMath(计算器类), 声明一个方法 Calculator()
  • 分别创建五个子类(加、减、乘,除,求余 五个子类),用于实现MyMath 接口
  • 在Main方法 定义两个变量,并提示从控制台输入运算符(+,-,*,/,%),实现输入不同的运算符调用不能的实现子类。
   视频教程:
誉尚学教育_誉尚学教育腾讯课堂官网 (qq.com)
或者:
C# 最强入门编程(.Net 学习系列开山巨作)_哔哩哔哩_bilibili
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

慢吞云雾缓吐愁

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表