【C#学习】25类的继承,类成员访问

  • 声明继承类的语法:在类名后加冒号[:],冒号后写【基类,或基接口】;基类和基接口统称为【类基础】

  • 术语规范:基类与派生类;父类与子类

  • 如果没有为一个类指明基类,实际上是派生自【System.Object】,该类是所有类型的基类,由于 .NET 类型系统是单根的,所以Object类处在所有类型继承链的顶端

【是一个(is a)】概念

一个子类的实例,从语义上讲,也是父类的一个实例
比如:一个学生,也是一个人;一辆汽车,也是一个交通工具…
在这里插入图片描述
可以用一个父类类型的变量去引用一个子类类型的实例

小知识点

(1)用 “sealed” 修饰的类称为【封闭类】,封闭类不能再当做基类使用

(2)C#系统中,一个类最多只能拥有一个基类,但可以实现多个基接口
规范:
【继承自,派生自】某个基类;【实现了】某个基接口

(3)子类的访问级别不能超越父类,可以跟父类的访问级别持平,或比起更低
比如说:不能出现父类的访问级别为【Internal】,而子类的访问级别却是【public】的情况

派生与继承对类成员的影响

继承的本质

继承的本质是派生类在基类已有的成员的基础之上,对基类进行的横向,纵向上的扩展
理解
(1)派生类基于基类目前已有的成员:当继承发生时,子类对父类的成员全盘获得

(2)在派生与继承的过程中,进行的操作是【扩展】:扩展,说明类成员只可能越来越多,不可能越来越少——可以添加类成员,但一旦添加后就不能再移除
一个类成员,一旦被引入到继承链当中,就会一直向下传递,不可能再被移除;所以在进行类设计/类库设计时,一定要非常小心,不要贸然引入新成员,否则很可能会对继承链,API造成污染
【类成员只能扩展,不能削减】是静态类性语言(比如C#,Java)的特点;动态类型语言(比如python,JS)随时都可以添加,移除类成员

(3)横向,纵向:【横向】指类成员在数量上的扩充;【纵向】指对类成员的版本进行扩充,版本扩充/版本更新称为【重写】

示例程序
1.子类对父类的类成员【全盘接受】,且类成员只能增加,不能移除
也就是说:不可能在继承链中的任何一个子类中将其父类的类成员移除,C#不支持这种操作,所以没有任何的关键字,操作符能够实现这种功能

2.什么是基类对象?

首先明确:在创建继承链上的某个类的实例时,会先触发基类的构造器,构造出【基类对象】
在这里插入图片描述

内存机理(猜想)
(1)【Car car】:当计算机看到【car】是一个引用类型的变量,立刻去【栈】中为其切割出4个字节的内存空间,并将内存中的值全部刷成0,表示该变量还没有引用任何实例

(2)【new Car】:表示为该类创建一个实例,计算机立刻去【堆】上寻找空余位置,并根据类成员切出相应内存空间,由于构造器还未被调用,所以此时这段内存还未被分配(Owner字段和ShowOwner()方法各占多少字节)和初始化

(3)【( )】:表示调用Car类的实例构造器,但在调用之前,会先去调用其基类【Vehicle的构造器】,就会在【堆内存】中创建出一个【基类对象】,由于Owner字段的数据类型 string 属于引用类型,所以基类对象所代表的这段内存(4个字节)中存储的是一个地址——Owner字段的实例在堆内存中的位置

(4)在基类的构造器中,将 “N/A” 赋值给Owner字段(也就是创建出了Owner字段的实例),“N/A” 这个实例在堆内存中的地址就传给了【基类对象】,那么基类对象所代表的内存块中就存储了值(这个值是 “N/A” 的地址),并继承给【new Car】所创建出来的实例

(5)由于new Car 所创建出来的实例继承了 “N/A” 的地址,所以此时该实例的Owner字段值也是 “N/A”

(6)接着开始调用派生类【Car类的构造器】:在Car的构造器中,将实例的Owner字段值【重写】为 “Dave”,注意:【基类对象和new Car出来的对象中存储的地址并没有改变,只是该地址所指向的内存块中存储的值发了生变化,由 "N/A"变为 “Dave”】

(7)完成了调用【Car类的构造器】,new Car 出的对象所代表的内存也就被成功分配和初始化了,这段内存所在的地址就会交还给【变量car】,从而完成变量car对实例的引用

(8)调用 car的ShowOwner()方法,打印出【car所引用的实例和基类对象的Owner字段值】,也就是this.Owner和base.Owner,应都为 “Dave”

(9)由于基类对象并没有变量引用,是一个【没有小孩牵着的气球】,所以一段时间后就会被垃圾收集器回收

简化示意图
在这里插入图片描述

提醒:
(1)属性是字段的包装器,属性的简化声明是语法糖,字段是存在的

(2)示意图中左侧橙色部分表示被操作系统占用的内存

(3)内存中的值(包括地址)皆用二进制表示

  • 为什么在创建子类对象时,会先调用父类构造器?
    因为创建一个实例,需要调用其构造方法,来初始化它的实例成员,而子类拥有父类的成员变量和成员方法,如果不去调用父类的构造器,那么这些从父类继承而来的成员则得不到正确的初始化

  • 如何调用父类构造器?
    子类构造器会默认调用父类的无参构造器,如果父类没有无参构造器(也就是自定了父类的构造器),则需在子类构造器的第一行显示调用父类的其他构造器,否则会报错
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    父类的实例构造器,是不能被子类所继承的

3.类成员的访问级别

原则:类成员的访问级别是以类的访问级别为上限的
比如说:如果一个类的访问级别为 Internal,就算该类的成员的访问级别为 public,在其他程序集中也是看不到这些类成员的

【注意区分】
类的访问级别:public,Internal
类成员的访问级别:public,Internal,private,protected
public级别和Internal级别在类中已经做过整理,这两个级别修饰类成员时可以很清晰地做出类比,故不再赘述

(1)private

【private】将成员的访问级别限制在【类体】中,是最低的访问级别

用private修饰的成员称为【私有成员】,父类的私有成员是不能被子类及子类对象所访问的;继承归继承,但不能访问
在这里插入图片描述

默认的访问级别是 【private】,这样做是为了保证数据的安全性,尽可能少地把数据和行为暴露到外界——封装
虽然是默认情况,但还是建议写上 “private” ,使代码更整齐明了

既然在子类中无法访问父类的私有成员,又如何证明该成员确实被继承了呢?
在这里插入图片描述

“_rpm”:可以理解为一种约定俗成的写法,当看到一个变量名前有下划线,立刻就会知道它是一个【实例字段,而且是私有字段】

(2)protected

【protected】会把类成员的访问级别限制在继承链上
当父类中的类成员是用 protected 修饰时,它的所有子类都可以访问该成员,但其他不在继承链上的类型是无法访问的

代码示例:
在这里插入图片描述

显然,Burn()方法的访问级别是不合理的;Refuel(),Accelerate()方法public 出去都可以,因为这两个方法可以且应该被外界所调用,加油和加速的操作应该是 “人” 来完成,但 Burn()方法不应该,【烧油是汽车自己的事】,是在汽车内部进行的,这个过程不是司机来完成的,也根本不该被司机看到——不能被司机所调用
所以,需要修改Burn()方法的访问级别为【protected】,保证其子类调用并继承就足够了

提醒:
在团队合作中,最好的不让类成员被别人调用的方法,就是别把这个类成员 public 出来,否则,如果对方没有阅读文档,或者文档写得不清楚,如果他发现调用这个成员可以解决自己的bug,那肯定会去调用的;这样一来,就可能会给自己以及整个团队造成麻烦

注意:
(1)【protected】是跨程序集的

(2)【protected】修饰符,更多地是应用于【方法】,因为类成员的纵向扩展(重写)和protected关系紧密

(3)【protected】和【Internal】可以组合,是 “或” 关系,顺序并无要求;也就是说:如果用 “protected” 和 “Internal” 共同修饰一个成员,那么该成员既可以被其派生类所访问,又可以被其所在的程序集当中的其他类所访问

  • 面向对象的实现风格
    包括:
    class—based
    prototype—based
    要以开放的心态去面对每一种编程语言
发布了29 篇原创文章 · 获赞 3 · 访问量 933

猜你喜欢

转载自blog.csdn.net/weixin_44813932/article/details/104054527
今日推荐