(一)继承的类型

1、实现继承和接口继承

在面向对象的编程中,有两种截然不同的继承类型:实现继承和接口继承。

  • 实现继承:表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数代码,除非在派生类型的定义中指定重写某个函数的实现代码。在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承非常有用。
  • 接口继承:表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要制定该类型具有某些可用的特性时,最好使用这种类型的继承。

2、多重继承

C#不支持多重实现继承,但允许类型派生自多个接口——多重接口继承。准确地说,因为System.Object是一个公共的基类,所以每个C#类(除Object类)都有一个基类,还可以有任意多个基接口。

3、结构和类

结构不支持实现继承,但支持接口继承。

定义结构和类可以总结为:

  • 结构总是派生自System.ValueType,它们还可以派生自任意多个接口。
  • 类总是派生自System.Object或用户选择的另一个类,它们还可以派生自任意多个接口。

(二)实现继承

声明派生自另一个类的类,语法如下:

例子:

以下代码创建了MyBaseClass类、MyClass类,其中MyClass类派生自MyBaseClass类

  1. class MyBaseClass
  2. {
  3. }
  4. class MyClass : MyBaseClass
  5. {
  6. }

如果类(或结构)也派生自接口,则用逗号分隔列表中的基类和接口:

  1. class MyClass : MyBaseClass,IInterface1,IInterface2
  2. {
  3. }

对于结构,语法如下:

  1. public struct MyStruct: IInterface1, IInterface2
  2. {
  3. }

1、虚方法

把一个基类函数声明为virtual,就可以在任何派生类中重写该函数:

  1. class MyBaseClass
  2. {
  3. public virtual string VirtualMethod()
  4. {
  5. return "虚方法";
  6. }
  7. }

在C#中,函数在默认情况下不是虚拟的,但(出构造函数以外)可以显式地声明为virtual。同时C#要求在派生类在重写基类中的函数时,要使用override关键字显式声明:

  1. class MyClass : MyBaseClass, IInterface1, IInterface2
  2. {
  3. public override string VirtualMethod()
  4. {
  5. return "重写了基类的虚方法";
  6. }
  7. }

2、隐藏方法

如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。

例子:

以下代码中,基类和派生类有一个相同的方法ShowName,当我们需要隐藏基类中的方法时应该显示地使用关键字new,否则编译器会发出警告。隐藏了基类方法,我们不能通过派生类访问基类方法,只能通过派生类自己访问。

  1. class MyBaseClass
  2. {
  3. public void ShowName(string name)
  4. {
  5. Console.WriteLine(name);
  6. }
  7. }
  8. class MyClass : MyBaseClass, IInterface1,
  9. {
  10. public new void ShowName(string name)
  11. {
  12. Console.WriteLine("派生类"+name);
  13. }
  14. }

在大多数情况下,我们应该重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险。

3、调用函数的基类版本

C#有一种特殊的语法用于从派生类中调用方法的基类版本:base.<MethodName>()。

例子:

以下代码在基类打印出名字的基础上,再打一行欢迎语

  1. class MyBaseClass
  2. {
  3. public virtual void ShowName(string name)
  4. {
  5. Console.WriteLine(name);
  6. }
  7. }
  8. class MyClass : MyBaseClass, IInterface1, IInte
  9. {
  10. public override void ShowName(string name)
  11. {
  12. base.ShowName(name);
  13. Console.WriteLine("欢迎您的光临!");
  14. }
  15. }

运行以上代码,结果如下:

4、抽象类和抽象函数

C#允许把类和函数声明为abstract。抽象类不能实例化,而抽象函数不能直接实现,必须在非抽象的派生类中重写。如果类包含抽象函数,则该类也是抽象的,也必须声明为抽象类。

例子:

以下代码创建了一个抽象的类和一个抽象方法

  1. abstract class MyAbstractClass {
  2. public abstract void FirstAbstractMethod();
  3. }

5、密封类和密封方法

C#允许把类和方法声明为sealed。对于类,这表示不能继承该类;对于方法,这表示不能重写该方法。

例子:

  1. sealed class FisrtSealedClass
  2. { }
  3. class ReadyClass : FisrtSealedClass
  4. { }

此时编译器会报错

把方法声明为sealed

例子:

  1. class MyBaseClass
  2. {
  3. public virtual void ShowName(string name)
  4. {
  5. Console.WriteLine(name);
  6. }
  7. }
  8. class MyClass : MyBaseClass, IInterface1, IInterface2
  9. {
  10. public sealed override void ShowName(string name)
  11. {
  12. base.ShowName(name);
  13. Console.WriteLine("欢迎您的光临!");
  14. }
  15. }

要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或属性。如果基类要密封,则不把它声明为virtual即可。

6、派生类的构造函数

例子:

  1. class MyBaseClass
  2. {
  3. public MyBaseClass() { }
  4. public MyBaseClass(int i) { }
  5. }
  6. class MyClass : MyBaseClass
  7. {
  8. public MyClass() { }
  9. public MyClass(int i) { }
  10. public MyClass(int i,int j) { }
  11. }

如果用以下方式实例化MyClass:

  1. MyClass myClass = new MyClass();

则构造函数的执行顺序如下:

  1. 执行System.Object.Object()构造函数;
  2. 执行MyBaseClass.MyBaseClass()构造函数;
  3. 执行MyClass.MyClass()构造函数。

如果用以下方式实例化MyClass:

  1. MyClass myClass = new MyClass(4);

则构造函数的执行顺序如下:

  1. 执行System.Object.Object()构造函数;
  2. 执行MyBaseClass.MyBaseClass()构造函数;
  3. 执行MyClass.MyClass(int i)构造函数。

最后,使用如下方式实例化MyClass:

  1. MyClass myClass = new MyClass(4, 8);

则构造函数的执行顺序如下:

  1. 执行System.Object.Object()构造函数;
  2. 执行MyBaseClass.MyBaseClass()构造函数;
  3. 执行MyClass.MyClass(int i,int j)构造函数。

还可以使用base()和this()来改变构造函数的执行顺序

  1. class MyBaseClass
  2. {
  3. public MyBaseClass() { }
  4. public MyBaseClass(int i) { }
  5. }
  6. class MyClass : MyBaseClass
  7. {
  8. public MyClass() { }
  9. public MyClass(int i) { }
  10. public MyClass(int i, int j) : base(i) { }
  11. }

这个时候我们在通过如下方式实例化MyClass:

  1. MyClass myClass = new MyClass(4, 8);

这个时候构造函数的执行顺序如下:

  1. 执行System.Object.Object()构造函数;
  2. 执行MyBaseClass.MyBaseClass(int i)构造函数;
  3. 执行MyClass.MyClass(int i,int j)构造函数。

base关键字指定.NET实例化过程使用基类中有指定参数的构造函数。

  1. class MyBaseClass
  2. {
  3. public MyBaseClass() { }
  4. public MyBaseClass(int i) { }
  5. }
  6. class MyClass : MyBaseClass
  7. {
  8. public MyClass() { }
  9. public MyClass(int i) : this(i, 5) { }
  10. public MyClass(int i, int j) { }
  11. }

这个时候我们在通过如下方式实例化MyClass:

  1. MyClass myClass = new MyClass(4);

这个时候构造函数的执行顺序如下:

  1. 执行System.Object.Object()构造函数;
  2. 执行MyBaseClass.MyBaseClass()构造函数;
  3. 执行MyClass.MyClass(int i,int j)构造函数;
  4. 执行MyClass.MyClass(int i)构造函数。

this关键字关键字指定.NET实例化过程使用当前类中有指定参数的构造函数。

但需要注意的是不要无限循环:

  1. class MyClass : MyBaseClass
  2. {
  3. public MyClass() : this(1){ }
  4. public MyClass(int i) : this() { }
  5. }

以上代码通过this相互指定,造成了无限循环。

通过以上代码的分析,我们可以知道构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到达到编译器要实例化的类为止。

无论在派生类上使用什么构造函数(默认构造函数或非默认构造函数),除非明确指定,否则就使用基类的默认构造函数。

(三)修饰符

修饰符,即应用于类型或成员的关键字。

1、可见性修饰符

修饰符 应用于 说明
public 所有类型或成员 任何代码均可以访问该项
protected 类型和内嵌类型的所有成员 只有派生的类型能访问该项
internal 所有类型或成员 只能在包含它的程序集中访问该项
private 类型和内嵌类型的所有成员 只能在它所属的类型中访问该项
protected internal 类型和内嵌类型的所有成员 只能在包含它的程序集和派生类型的任何代码中访问该项

2、其他修饰符

修饰符 应用于 说明
new  函数成员 成员用相同的签名隐藏继承的成员
static 所有成员 成员不作用与类的具体实例
virtual 仅函数成员 成员可以由派生类重写
abstract 仅函数成员 虚拟成员定义了成员的签名,但没有提供实现代码
override 仅函数成员 成员重写了继承的虚拟或抽象成员
sealed 类、方法和属性 对于类,不能继承自密封类。对于属性和方法,成员重写已继承的虚拟成员,但任何派生类中的成员都不能重写该成员。该修饰符必须与override一起使用
extern 仅静态[DllImport]方法 成员在外部用另一种语言实现

(四)接口

如果一个类派生自一个接口,声明这个类就会实现某些函数。

接口只能包含方法、属性、索引器和事件的声明。

不能实例化接口,它只能包含其成员的签名。

接口总是共有的,不能声明为虚拟或静态的。

接口通常以字母I开头,以便知道这是一个接口。

1、定义和实现接口

首先我们定义一个接口,该接口功能是一个说话的接口

  1. interface ITalk {
  2. void talk(string str);
  3. }

接下来我们通过类Chinese来实现该接口

  1. class Chinese : ITalk
  2. {
  3. public void talk(string str)
  4. {
  5. Console.WriteLine("中文语音:" + str);
  6. }
  7. }

还可以定义一个American来实现该接口

  1. class American : ITalk {
  2. public void talk(string str)
  3. {
  4. Console.WriteLine("English:" + str);
  5. }
  6. }

通过接口的继承,我们实现了不同国家的人都可以说话这样一个功能,外部调用时我们通过接口引用可以很方便的调用。

2、接口的派生

当ITalk不满足需求时,假定这个时候需要给Chinese和American添加一个Speak功能时,可以通过接口的派生来实现

  1. interface ILanguage : ITalk
  2. {
  3. void Speak(string str);
  4. }

改变后的接口实现

  1. class Chinese : ILanguage
  2. {
  3. public void Speak(string str)
  4. {
  5. Console.WriteLine("中文演讲:" + str);
  6. }
  7.  
  8. public void talk(string str)
  9. {
  10. Console.WriteLine("中文语音:" + str);
  11. }
  12. }
  13. class American : ILanguage
  14. {
  15. public void Speak(string str)
  16. {
  17. Console.WriteLine("English Speak:" + str);
  18. }
  19.  
  20. public void talk(string str)
  21. {
  22. Console.WriteLine("English:" + str);
  23. }
  24. }

【读书笔记】C#高级编程 第四章 继承的更多相关文章

  1. 读书笔记 - js高级程序设计 - 第四章 变量 作用域 和 内存问题

      5种基本数据类型 可以直接对值操作 判断引用类型 var result = instanceof Array 执行环境 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这 ...

  2. unix环境高级编程第四章笔记

    文件和目录 start fstart lstart函数 一旦给出pathname, start函数就返回了与此命名文件有关的信息结构 #include <sys/start> int st ...

  3. UNIX系统高级编程——第四章-文件和目录-总结

    文件系统: 以UNIX系统V文件系统为例: 磁盘分为区,每个分区都有自己的文件系统: ​ i节点是固定长度的记录项,包含了文件的相关信息.目录项包含文件名和i节点号.stat结构中除文件名和i节点编号 ...

  4. 读书笔记 - js高级程序设计 - 第十一章 DOM扩展

      对DOM的两个主要的扩展 Selectors API HTML5  Element Traversal 元素遍历规范 querySelector var body = document.query ...

  5. 读书笔记 - js高级程序设计 - 第七章 函数表达式

      闭包 有权访问另一个函数作用域中的变量的函数 匿名函数 函数没有名字 少用闭包 由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存.过度使用闭包可能会导致内存占用过多,我们建议读者 ...

  6. 读书笔记 - js高级程序设计 - 第六章 面向对象的程序设计

      EcmaScript有两种属性 数据属性 和 访问器属性 数据属性有4个特性 Configurable Enumerable Writable Value   前三个值的默认值都为false   ...

  7. 读书笔记 - js高级程序设计 - 第五章 引用类型

      引用类型 和 类 不是一个概念 用typeof来检测属性是否存在 typeof args.name == "string"  需要实验 访问属性的方法 .号和[] 一般情况下要 ...

  8. 读书笔记 - js高级程序设计 - 第三章 基本概念

    启用严格模式 "use strict" 这是一个 pragma 编译指示 让编码意图更清晰  是一个重要原则 5种简单数据类型 Undefined Null Boolean Num ...

  9. 读书笔记 - js高级程序设计 - 第十五章 使用Canvas绘图

    读书笔记 - js高级程序设计 - 第十三章 事件   canvas 具备绘图能力的2D上下文 及文本API 很多浏览器对WebGL的3D上下文支持还不够好   有时候即使浏览器支持,操作系统如果缺缺 ...

随机推荐

  1. 03 转换css元素的类别

    03 转换css元素的类别 通过设置display属性 属性 作用 block 块级 inline 行内 inline-block 行内块级 接来下 就跟着小demo来学习吧! 不懂可以看看!!!什么 ...

  2. 4种方法教你如何查看java对象所占内存大小

    摘要:本文讲述4种查看java对象所占内存大小的方法 本文分享自华为云社区<查看java对象所占内存大小>,作者:xiewenci. 计算java对象所占内存大小 1.使用jdk8自带AP ...

  3. Hdfs存储策略

    一.磁盘选择策略 1.1.介绍 在HDFS中,所有的数据都是存在各个DataNode上的.而这些DataNode上的数据都是存放于节点机器上的各个目录中的,而一般每个目录我们会对应到1个独立的盘,以便 ...

  4. Day03 HTML标记

    文本标题 <h1>一级标题</h1> <h2>二级标题</h2> <h3>三级标题</h3> <h4>四级标题< ...

  5. Trie 树进阶学习笔记

    前言 没脑子选手发现自己什么都不会 ... \(\text{More and more vegetables, What should I do?}\) 正文 Trie 树简介 大概是人类的话都知道吧 ...

  6. JDBCTools 第一个版本

    JDBCToolV1: package com.dgd.test; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax ...

  7. 线程池的概念&原理和线程池的代码实现

    线程池:一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源.工作原理:可以用一张图来简洁明了说明: 合理利用线程池能够带来三个好处∶1.降低 ...

  8. vue封装原生的可预览裁剪上传图片插件H5,PC端都可以使用

    思路:1.先做出一个上传的图片的上传区 <!-- 上传区 --> <label for="fileUp"> <div class="upBo ...

  9. C++指针和结构体基础知识

    学习C++首先要回忆起C语言当中的指针和结构体知识,本文作者将通过一段代码来总结指针和结构体基础知识:指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址.就像其他变量或常量一样,您必须在使 ...

  10. NOI / 2.5基本算法之搜索-6044:鸣人和佐助详解

    总时间限制: 1000ms 内存限制: 65536kB 题目 佐助被大蛇丸诱骗走了,鸣人在多少时间内能追上他呢? 已知一张地图(以二维矩阵的形式表示)以及佐助和鸣人的位置.地图上的每个位置都可以走到, ...