0
点赞
收藏
分享

微信扫一扫

第14章 继 承

中间件小哥 2022-03-21 阅读 54
c#

第14章 继 承

  • 继承的引入,就是在类之间建立一种相交关系,使得新定义的派生类的实例可以继承已有的基类的特征和能力,而且可以加入新的特性或者是修改已有的特性,建立起类的层次。
  • 多态 —— 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。多态性通过派生类重载基类中的虚拟函数型方法来实现。

14.1 C#的继承机制

14.1.1 概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O8jiykqp-1647831987520)(1401.jpg)]
最高层的实体往往具有最一般最普遍的特性,越下层的事物越具体,并且下层包含了上层的特征,它们之间的关系是基类与派生类之间的关系。
—— 为了用软件语言对现实世界的层次结构进行模型化,面向对象的程序设计技术引入了继承的概念。一个类从另一个类派生出来的,派生类从基类哪里继承特性。也可以作为其它类的基类。从一个基类派生出来的多层类形成了类的层次结构。

  • c# 中派生类只能从一个类中继承。派生类从它的基类中继承成员: 方法,域,属性,事件,索引指示器。 除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。

程序清单 14-1

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/*
vehicle 做为基类,体现了“汽车”这个实体具有的公共性质;汽车都有轮子和重量。Car类继承了vehicle 的性质
并且添加了自身的特性——可搭载乘客。
*/

namespace _14_1
{
    class vehicle       //定义汽车类
    {
    public int wheels; // 公有成员:轮子个数 —— 如果少了 public ?
        protected float weight;//   保护成员:重量
        public vehicle() {; }
        public  vehicle (int w,float g)
        {
            wheels = w;
            weight = g;
        }
        public void Speak()
        {
            Console.WriteLine("the w vehicle is speaking!");
        }
    }
    class Car:vehicle  //定义轿车类:从汽车类中继承
    {
        int passenger;  //私有成员:乘客数
        public Car(int w, float g, int p) : base(w, g) // 
        {
            wheels = w;
            weight = g;
            passenger = p;
        }
    }
}
  1. 继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object 类作为所有类的基类。
  2. 派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。
  3. 构造函数和析构函数不能被继承。除此以外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
  4. 派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖以继承的成员。但这并不因为派生类删除了这些成员,只是不能在访问这些成员。
  5. 类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。

14.1.2 覆盖

我们上面提到,类的成员声明中,可以声明与继承而来的成员同名的成员。这时我们称派生类的成员覆盖(hide)了基类的成员。这种情况下,编译器不会报告错误,但会给出一个警告。对派生类的成员使用new 关键字,可以关闭这个警告。
程序清单 14-2:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/*
派生类的成员覆盖hide 了基类的成员这种情况下编译器不会报告错误
但会给出一个警告对派生类的成员使用new 关键字可以关闭这个警告
前面汽车类的例子中类Car 继承了Vehicle 的Speak 方法我们可以给Car
类也声明一个Speak 方法覆盖Vehicle 中的Speak
*/

namespace _14_2
{
    internal class Vehicle
    {
        public int wheels;  //公有成员:轮子个数
        protected float weight;     //保护成员:重量
        public Vehicle() {; }
        public Vehicle(int w,float g)
        {
            wheels = w;
            weight = g;
        }
        public void Speak()
        {
            Console.WriteLine("the w vehicle is speaking!");
        }

    }
    class Car : Vehicle     //定义轿车类
    {
        int passengers;     //私有成员:乘客数
        public Car(int w, float g, int p) : base(w, g)
        {
            wheels = w;
            weight = g;
            passengers = p;
        }
        new public void Speak()
        {
            Console.WriteLine("Di-di!");
        }
    }
    /*
    如果在成员声明中加上了new 关键字修饰而该成员事实上并没有覆盖继
承的成员编译器将会给出警告在一个成员声明同时使用new 和 override 则编译器
会报告错误
    */
}

14.1.3 base 保留字

base 关键字主要是为派生类调用基类成员提供一个简写的方法。

class A
{
public void F()
{
// F 的具体执行代码
}
public int this[int nIndex]
{
get{};
set{};
}
}
class B
{
public void G()
{
int x = base[0];
base.F();
}
}

类B 从A类中继承,B的方法G中调用了A的方法F和索引指示器。方法F在进行编译时等价于:
public void G()
{
int x = (A this )[0];
(A this ).F();
}
使用base 关键字对基类成员的访问格式为
base . identifier
base [ expression-list ]

14.2 多 态 性

多态性是一个非常重要的概念,它允许客户对一个对象进行操作,有对象来完成一系列的动作,具体实现那个动作、如何实现有系统负责解释。

14.2.1 C#中的多态性。

  • c# 中,多态性的定义是—— 同一操作作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的执行结果。C#支持两种类型的多态性:

    编译时的多态性,—— 是根据重载来实现的。
    运行时的多态性,—— 是指直到系统运行时,才根据实际情况决定实现何种操作。通过虚成员实现。

    编译时的多态性为我们提供了运行速度快的特点,运行是的多态性则带来高度灵活和抽象的特点。

    14.2.2 虚方法

    当类中的方法声明前加上了 Virtual 修饰符,我们称之为虚方法,反之为非虚。使用了Virtual 修饰后,不允许再有static 、abstract、override 修饰符。
    对于非虚的方法,无论被其所在类的实例调用,还是被这类的的派生类的实例调用,方法的执行方式不变。而对于虚方法,它的执行方式可以被派生类改变,通过方法的重载来实现的。

    程序清单 14-3:

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
    
      namespace _14_3
      {
          class A
          {
              /// <summary>
              /// 非虚方法 F
              /// </summary>
              public void F()
              {
                  Console.WriteLine("A.F");
              }
              /// <summary>
              /// 虚方法 G
              /// </summary>
              public virtual void G()
              {
                  Console.WriteLine("A.G");
              }
          }
          class B:A
          {
              /// <summary>
              /// 类B 提供了一个新的非虚的方法F,
              /// 从而覆盖了继承的F; 
              /// </summary>
              new public void F()
              {
                  Console.WriteLine("B.F");
              }
    
              /// <summary>
              /// 类B 同时还重载了继承的方法G。
              /// </summary>
              public override void G()
              {
                  Console.WriteLine("B.G");
              }
          }
      }
    

    /*
    对于非虚的方法无论被其所在类的实例调用还是被这个类的派生类的实例调

    用方法的执行方式不变而对于虚方法它的执行方式可以被派生类改变这种改
    变是通过方法的重载来实现的
    */

      internal class Program
      {
          static void Main(string[] args)
          {
              B b = new B();            
              A a = b;
              a.F();
              b.F();
              a.G();  //方法a.G() 实际调用了B.G,而不是A.G。这是因为编译时值为A,但运行时值为B,
                      //所有B完成了对方法的实际调用。
              b.G();
          }
    
      }
    

    14.2.3 在派生类中对虚方法进行重载

    普通的方法重载指的是:类中两个以上的方法,取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用那个方法。
    —— 对基类虚方法的重载是函数重载的另种特殊形式。在派生类中重新定义此虚函数时,要求的是方法名称、返回值类型、参数表中的参数个数、类型、顺序都必须与基类中的虚函数完全一致。在派生类中声明对虚方法的重载,要求在声明中加上override 关键在,而且不能有new,static 或 virtual 修饰符。

    程序清单 14-4:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace _14_4
{
public class Vehicle
{
public int wheels; // 公有成员: 轮子
protected float weight; //保护成员: 重量
public Vehicle(int w, float g)
{
wheels = w;
weight = g;
}

    /// <summary>
    /// 声明虚方法,那么在派生类中就可以重新定义此方法。
    /// </summary>
    public virtual void Speak()
    {
        Console.WriteLine("the w vehicle is speaking!");
    }
}
public class Car : Vehicle  //定义轿车类
{
    int passenger;      //私有成员: 乘客数
    public Car(int w, float g, int p) : base(w, g)
    {
        wheels = w;
        weight = g;
        passenger = p;
    }
    
    /// <summary>
    /// 在派生类Car 和Truck中分别重载了Speak方法,
    /// 派生类中的方法原型和基类中的方法原型必须完全一致。
    /// </summary>
    public override void Speak()
    {
        Console.WriteLine("The car is speaking :Di-di!");
    }
}
class Truck : Vehicle       //定义卡车类
{
    int passengers;         //私有成员:乘客数
    float load;             //私有成员:载重量
    public Truck(int w, float g, int p, float l) : base(w, g)
    {
        wheels = w;
        weight = g;
        passengers = p;
        load = l;
    }
    public override void Speak()
    {
        Console.WriteLine("The truck is speaking :Ba-ba!");
    }
}

}

internal class Program
{
    /// <summary>
    /// 创建了Vehicle类的实例v1,
    /// 并且先后Car类的实例 C1和Truck 类的实例t1.
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        Vehicle v1 = new Vehicle(1,5);
        Car c1 = new Car(4, 2,5);
        Truck t1 = new Truck(6,5,3,10);
        v1.Speak();
        v1 = c1;
        v1.Speak();
        c1.Speak();

        v1 = t1;
        v1.Speak();
        t1.Speak();
        /*
         Vehicle 类的实例v1 先后被赋予Car 类的实例c1 ,以及Truck 类的实例t1 的值。在执行过程中,
        v1 的 Speak 方法实现了多态性,并且v1.Speak() 究竟执行那个版本,不是在陈谷编译时确定的,而是
        在程序的动态运行时,根据v1某一时刻的指代类型来确定,所有还体现了动态的多态性。
         */

    }
}

}

14.3 抽象与密封

14.3.1 抽象类

有时候,基类并不与具体的事物相联系,而是指表达一种抽象的概念,用以为它的派生类提供一个公共的界面。 (abstract class)的概念。

  • 抽象类使用abstract修饰符,对抽象类的使用以下几点规定:
    抽象类只能作为其他类的基类,他不能直接被实例化,而且对抽象类不能使用 new 操作符。抽象类如果含有抽象的变量或值,则它们要么是null 类型,要么包含了对非抽象类的实例的引用。

  • 抽象类允许包含抽象成员,虽然这不是必须的。

  • 抽象类不能同时又是密封的。
    如果一个非抽象类从抽象类中派生,则其必须通过重载来实现所有继承而来的抽象成员。

    abstract class A
    {
    public abstract void F();
    }
    abstract class B: A
    {
    public void G() {}
    }
    class C: B
    {
    public override void F()
    {
    // F 的具体实现代码
    }
    }

    抽象类A 提供了一个抽象方法F 类B 从抽象类A 中继承并且又提供了一个方
    法G 因为B 中并没有包含对F 的实现所以B 也必须是抽象类类C 从类B 中继承
    类中重载了抽象方法F 并且提供了对F 的具体实现则类C 允许是非抽象的
    让我们继续研究汽车类的例子我们从交通工具这个角度来理解Vehicle 类的
    话它应该表达一种抽象的概念我们可以把它定义为抽象类由轿车类Car 和卡车
    类Truck 来继承这个抽象类它们作为可以实例化的类

程序清单 14-5

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/*
我们从交通工具这个角度来理解Vehicle 类的
话它应该表达一种抽象的概念我们可以把它定义为抽象类由轿车类Car 和卡车
类Truck 来继承这个抽象类它们作为可以实例化的类
*/
namespace _14_5
{
    
    internal class vehicle      //定义汽车类
    {
        public int wheels;      //公有成员:轮子个数
        protected float weight; //保护成员:重量
        public vehicle(int w,float g)
        {
            wheels = w;
            weight = g;
        }
        public virtual void Speak()
        {
            Console.WriteLine("the w vehicle is spaeking!");
        }
    }
    class Car : vehicle     //定义轿车类
    {
        int passengers;     //私有成员:乘客数
        public Car(int w, float g, int p) : base(w, g)
        {
            wheels = w;
            weight = g;
            passengers = p;
        }
        public override void Speak()
        {
            Console.WriteLine("The car is speaking:Di-di!");
        }
    }
    class Truck:vehicle     //定义卡车
    {
        int passengers;     //私有成员:乘客数
        float load;     // 私有成员:载重量
        public Truck(int w,float g,int p,float l):base(w,g)
        {
            wheels = w;
            weight = g;
            passengers = p;
            load = l;
        }
        public override void Speak()
        {
            Console.WriteLine("The truck is speaking:Ba-ba!");
        }
    }
}

14.3.2 抽象方法

由于抽象类本身表达的是抽象的概念,因此类中的许多方法并不一定要有具体的实现,而只是留出一个接口来作为派生类重载的界面。举一个简单的例子,“图形”这个类是抽象的,它的成员方法“计算图形面积”也就没有实际的意义。面积只对“图形”的派生类不如“圆”,“三角形”这些非抽象的概念才有效,那么我们就可以把基类“图形”的成员方法“计算面积”声明为抽象的,具体的实现交给派生类通过重载来实现。
    一个方法声明中如果加上 abstract 修饰符,我们称该方法为抽象方法(abstractmethod)。
如果 一个方法被声明也是抽象的,那么该方法默认也是一个虚方法。事实上,抽象方法是一个新的虚方法,它不提供具体的方法实现代码。我们知道,非虚的派生类要求通过重载为继承的虚方法提供自己的实现,而抽象方法则不包含具体的实现内容,所有方法声明的执行体中只有一个分号";"
    只能在抽象类中声明抽象方法。对抽象方法,不能再使用static 或virtual 修饰符,而且方法不能有任何可执行代码,哪怕只是一对大括号中间加一个一个分号{;}
    都不允许出现,只需要给出方法的原型就可以了。

程序清单 14-6:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace _14_6
{
    abstract class vehicle  //定义汽车类
    {
        public int wheels;      //公有成员;轮子个数
        protected float weight;     //保护成员;重量
        public vehicle(int w,float g)
        {
            wheels = w;
            weight = g;
        }
        public abstract void Speak()
            ;
    }
    class Car:vehicle       //定义轿车类
    {
        int passengers; // 私有成员;乘客数
        public Car(int w,float g,int p):base(w,g)
        {
            wheels = w;
            weight = g;
            passengers = p;
        }
        public override void Speak()
        {
            Console.WriteLine("The car is speaking : Di-di!");
        }
    }
    class Truck:vehicle     //定义卡车类
    {
        int passengers;     //私有成员:乘客数
        float load;         //私有成员;载重量
        public Truck(int w,float g,int p,float l):base(w,g)
        {
            wheels = w;
            weight = g;
            passengers = p;
            load = l;

        }
        public override void Speak()
        {
            Console.WriteLine("The truck is speaking:Ba-ba!");
        }
    }
}


还要注意,抽象方法在派生类中不能使用 base 关键字来进行访问。

    class A
    {
        public abstract void F();
    }
    class B: A
    {
        public override void F() 
        {
            base.F(); // 错误 base.F 是抽象方法
        }
    }

我们还可以利用抽象方法来重载基类的虚方法这时基类中虚方法的执行代码就
被拦截了下面的例子说明了这一点

class A
{
    public virtual void F() 
    {
        Console.WriteLine("A.F");
    }
}

abstract class B: A
{
    public abstract override void F();
}

class C: B
{
    public override void F() 
    {
        Console.WriteLine("C.F");
    }
}

类A 声明了一个虚方法F 派生类B 使用抽象方法重载了F 这样B 的派生类C
就可以重载F 并提供自己的实现

14.3.3 密封类

想想看如果所有的类都可以被继承继承的滥用会带来什么后果类的层次结
构体系将变得十分庞大类之间的关系杂乱无章对类的理解和使用都会变得十分困
难有时候我们并不希望自己编写的类被继承另一些时候有的类已经没有再被
继承的必要C#提出了一个密封类sealed class 的概念帮助开发人员来解决这一
问题
密封类在声明中使用sealed 修饰符这样就可以防止该类被其它类继承如果试
图将一个密封类作为其它类的基类C#将提示出错理所当然密封类不能同时又是
抽象类因为抽象总是希望被继承的
在哪些场合下使用密封类呢密封类可以阻止其它程序员在无意中继承该类而
且密封类可以起到运行时优化的效果实际上密封类中不可能有派生类如果密封
类实例中存在虚成员函数该成员函数可以转化为非虚的函数修饰符virtual 不再生

让我们看下面的例子
abstract class A
{
public abstract void F();
}
sealed class B: A
{
public override void F()
{
// F 的具体实现代码
}
}
如果我们尝试写下面的代码
class C: B{ }
C#会指出这个错误告诉你B 是一个密封类不能试图从B 中派生任何类

14.3.4 密封方法

使用密封类可以防止对类的继承C#还提出了密封方法sealed
method 的概念以防止在方法所在类的派生类中对该方法的重载
对方法可以使用sealed 修饰符这时我们称该方法是一个密封方法
不是类的每个成员方法都可以作为密封方法密封方法必须对基类的虚方法进行
重载提供具体的实现方法所以在方法的声明中sealed 修饰符总是和 override 修饰符同时使用。
程序清单 14-7:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace _14_7
{
internal class A
{
public virtual void F()
{
Console.WriteLine(“A.F”);
}
public virtual void G()
{
Console.WriteLine(“A.G”);
}
}
class B:A
{
public sealed override void F()
{
Console.WriteLine(“B.F”);
}
public override void G()
{
Console.WriteLine(“B.G”);
}
}
class C:B
{
public override void G()
{
Console.WriteLine(“C.G”);
}
}
/*
类B 对基类 A 中的两个虚方法均进行了重载,其中 F 方法使用了 sealed 修饰符,
成为一个密封方法。G 方法不是密封方法,所以在 B 的派生类C中,可以重载方法G,但
不能重载方法F。
*/
}

14.4 继承中关于属性的一些问题

和类的成员方法一样我们也可以定义属性的重载虚属性抽象属性以及密封
属性的概念
与类和方法一样属性的修饰也应符合下列规则
属性的重载
􀁺 在派生类中使用修饰符的属性表示对基类中的同名属性进行重载
􀁺 在重载的声明中属性的名称类型访问修饰符都应该与基类中被继承的
属性一致
􀁺 如果基类的属性只有一个属性访问器重载后的属性也应只有一个但如果
基类的属性同时包含get 和set 属性访问器重载后的属性可以只有一个也可以同时
有两个属性访问器
注意与方法重载不同的是属性的重载声明实际上并没有声明新的属性而只
是为已有的虚属性提供访问器的具体实现
虚属性
􀁺 使用 virtual 修饰符声明的属性为虚属性
􀁺 虚属性的访问器包括get 访问器和set 访问器同样也是虚的
抽象属性
􀁺 使用 abstract 修饰符声明的属性为抽象属性
􀁺 抽象属性的访问器也是虚的而且没有提供访问器的具体实现这就要求在
非虚的派生类中由派生类自己通过重载属性来提供对访问器的具体实现

􀁺 abstract 和override 修饰符的同时使用不但表示属性是抽象的而且它重载
了基类中的虚属性这时属性的访问器也是抽象的
􀁺 抽象属性只允许在抽象类中声明
􀁺 除了同时使用abstract 和override 修饰符这种情况之外static, virtual, override
和abstract 修饰符中任意两个不能再同时出现
密封属性
􀁺 使用 sealed 修饰符声明的属性为密封属性类的密封属性不允许在派生类中被
继承密封属性的访问器同样也是密封的
􀁺 属性声明时如果有sealed 修饰符同时也必须要有override 修饰符
从上面可以看出属性的这些规则与方法十分类似对于属性的访问器我们可
以把get 访问器看成是一个与属性修饰符相同没有参数返回值为属性的值类型的方
法把set 访问器看成是一个与属性修饰符相同仅含有一个value 参数返回类型为
void 的方法还记得第十章中客户住宿的例子吗还是让我们扩展这个例子来说明属
性在继承中的一些问题

程序清单 14-8

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace _14_8
{
    public enum sex
    {
        woman,
        man,
    };
    abstract public class People
    {
        private string s_name;
        public virtual string Name
        {
            get { return s_name; }
        }
    }
    private sex m_sex;
    public virtual sex Sex{
        get{ return m_sex; }
    }
    protected string s_card;
    public abstract string Card
    {
        get;set;
    }
}

程序清单 14-9

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace _14_9
{
    class Custome: People
    {
        string s_no;
        int i_day;
        public string No
        {
            get { return s_no; }
            set
            {                
                if (s_no != value)
                {
                    s_no = value;
                }
            }
        }
        public int Day 
        { 
            get { return i_day; } 
            set 
            {
                if (i_day != value)
                {
                    i_day = value;
                }
            }
        }
        public override string Name
        {
            get { return base.Name; }
        }
        public override sex Sex
        {
            get { return base.Sex; }
        }
        public override string Card
        {
            get { return s_card; }
            set { s_card = value; }
        }
    }
}

在类Customer 中属性Name Sex 和Card 的声明都加上了override 修饰符属
性的声明都与基类People 中保持一致Name 和Sex 的get 访问器Card 的get 和set
访问器都使用了base 关键字来访问基类People 中的访问器属性Card 的声明重载了
基类People 中的抽象访问器这样在Customer 类中没有抽象成员的存在Customer
可以是非虚的

14.5 小 结

继承是面向对象系统中一个非常重要的概念C#语言为我们提供了一整套设计良好的继承机制包括:

  • 派生类对基类的继承
  • 方法的继承
  • 属性及其访问器的继承
    在C#中还提供了抽象和密封的概念给继承方式带来了高度的灵活性大大方便
    了开发人员设计自己的类的层次结构体系包含了抽象方法或抽象属性的类必须是抽
    象类抽象类的这些成员交给派生类去实现密封类不允许被继承密封方法和密封
    属性不允许被重载抽象和密封的概念是本章的难点希望读者认真掌握。
举报

相关推荐

0 条评论