【C#小知识】C#中一些易混淆概念总结(六)---------解析里氏替换原则,虚方法

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

目录:

【C#小知识】C#中一些易混淆概念总结--------数据类型存储位置,方法调用,out和ref参数的使用

【C#小知识】C#中一些易混淆概念总结(二)--------构造函数,this关键字,部分类,枚举

【C#小知识】C#中一些易混淆概念总结(三)--------结构,GC回收,静态成员,静态类

【C#小知识】C#中一些易混淆概念总结(四)---------解析Console.WriteLine()

C#小知识】C#中一些易混淆概念总结(五)---------深入解析C#继承

----------------------------------分割线--------------------------------------

这一系列的文章在园子里还是比较受欢迎的。有一些留言指出了其中理论性的错误,怎么写出来这些文章的,有没有培训过等等问题。

下面就一并的回答这些问题吧。
1)自己今年六月份毕业,现在在帝都实习。不过在学校已经做过一些C#开发了,现在也是做.NET开发工作。

2)文章中很多知识是自己以前在网上下载的视频教程,学习过程中所记的笔记。也就是在年前的时候,突然有一天发现自己的笔记本记了差不多块一本了,之前也没时间整理过,所以就想着把它们整理成博客文章,顺便温习一下这些笔记知识。

3)有园友问自己是不是在传智培训过。首先说我没有培训过,但是非常感谢传智公开的一些自学教程。因为自己也是这些视频的受益者,学到了很多知识,养成了一些好的学习习惯。

4)在整理笔记的过程中遇到了很多问题,其中自己参考了《C#本质论》,《CLR via C#》还有就是MSDN的官方文档。

3)不管怎样还是会遇到一些自己解决不掉或者弄不清楚的问题,这个过程使用了Google搜索并和请教了一些园友。

4)错误总是会存在。谢谢看我博客的读者你们的细心,指出了我博文中的错误。确定这些错误后,我都立即修改了自己的文章。

---------------------------------------------分割线-----------------------------------------------------

今天开始上班了。这几天研究学习了一下思维导图,感觉用它整理自己的知识非常的方便。所以,以后写博客完成一个知识点,都会用思维导图做一个总结。也能让大家对所要读的内容有一个整体的把握。

我用的思维导图软件是FreeMind(免费的,但是得装JDK),因为刚开始学习使用,很多操作技巧不是很熟练,做出来的导图估计也不是很好,希望大家见谅。

首先,里氏替换原则。

这是理解多态所必须掌握的内容。对于里氏替换原则维基百科给出的定义如下:

072225093508573.png

为什么子类可以替换父类的位置,而程序的功能不受影响呢?

当满足继承的时候,父类肯定存在非私有成员,子类肯定是得到了父类的这些非私有成员假设,父类的的成员全部是私有的,那么子类没办法从父类继承任何成员,也就不存在继承的概念了)。既然子类继承了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员。所以,子类对象可以替换父类对象的位置。

来看下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class  Program
     {
         static  void  Main( string [] args)
         {
             Person p =  new  Person();
             Person p1 =  new  Student();
             Console.ReadKey();
         }
     }
     class  Person
     {
     //父类的私有成员
     private  int  nAge;
         public  Person()
         {
             Console.WriteLine( "我是Person构造函数,我是一个人!" );
         }
         public  void  Say()
         {
             Console.WriteLine( "我是一个人!" );
         }
     }
     class  Student : Person
     {
         public  Student()
         {
             Console.WriteLine( "我是Student构造函数,我是一个学生!" );
         }
         public  void  SayStude()
         {
             Console.WriteLine( "我是一个学生!" );
         }
     }
     class  SeniorStudent : Student
     {
         public  SeniorStudent()
         {
             Console.WriteLine( "我是SeniorStudent构造函数,我是一个高中生!" );
         }
         public   void  SaySenior()
         {
             Console.WriteLine( "我是一个高中生!" );
         }
     }

我们运行打印出的结果是:

072240214751281.png

根据前面的构造函数的知识很容易解释这个结果。那么我们在Main()函数中添加如下的代码:

1
2
3
4
5
6
7
8
9
static  void  Main( string [] args)
         {
             Person p =  new  Person();
             p.Say();
  
             Person p1 =  new  Student();
             p1.Say();
             Console.ReadKey();
         }

在访问的过程中,可以发现p只可以访问父类的say

072249159884254.png

而p1也只可以访问父类的Say方法

072247273693436.png

其实在上面的代码中,就满足了里氏替换原则。子类的Student对象,替换了父类Person对象的位置。


那么它们在内存中发生了些什么呢?如下图:

072355391281450.png

由上可以知道,当一个父类的变量指向一个子类对象的时候只能通过这个父类变量调用父类成员,子类独有的成员无法调用。

同理我们可以推理出,子类的变量是不可以指向一个父类的对像的

080009598379294.png

但是当父类变量指向一个子类变量的时候,可以不可以把父类的变量转化成子类的对象呢?看下图

080020564982450.png

关于引用类型的两种转换方式:

由上面的代码我们已经知道了一种转换,就是在变量钱直接加需要转换的类型,如下代码:

Student s2 = (Student)p1;

那么第二种转换方式就是使用as关键字,如下代码:

//将指向子类对象的变量转化成子类类型
Student s2 = (Student)p1;            
//使用as关键字,转换失败返回一个null值
Student s3 = p1 as Student;

使用as关键字和第一种强制转换的区别就是,第一种如果转换失败会抛异常,第二种转换失败则返回一个null值。

思维导图总结如下:

080046083779396.png


二,虚方法

使用virtual关键字修饰的方法,叫做虚方法(一般都是在父类中)。

看下面的一段代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class  Person
     {
         private  int  nAge;
         public  Person()
         {
             Console.WriteLine( "我是Person构造函数,我是一个人!" );
         }
         //这里定义了一个虚方法
         public  virtual  void  Say()
         {
             Console.WriteLine( "我是一个人!" );
         }
     }
     class  Student : Person
     {
         //子类使用override关键字改写了父类的虚方法
         public  override  void  Say()
         {
             Console.WriteLine( "我是一个学生!" );
         }
         public  Student()
         {
             Console.WriteLine( "我是Student构造函数,我是一个学生!" );
         }
         public  void  SayStude()
         {
             Console.WriteLine( "我是一个学生!" );
         }
     }


紧接着在main()函数中添加如下的代码:

1
2
3
4
5
6
7
8
9
10
static  void  Main( string [] args)
         {
             Person p =  new  Person();
             p.Say();
             Person p1 =  new  Student();
             p1.Say();
             Student s =  new  Student();
             s.Say();
             Console.ReadKey();
         }

打印结果如下:


080113432039737.png


我们很明显的可以发现,第二个表达式满足里氏替换原则,p1.Say()执行的应该是父类的Say()方法,但是这里却执行了子类的Say()方法。

这就是子类使用override关键字的Say()方法覆盖了父类的用Virtual关键字修饰的Say()方法。

我们使用动态图片看一下调试过程,

①首先是没有使用任何关键字:

080123570655066.gif


由上可以看出直接跳入父类,执行了父类的Say()方法;


②再看使用virtual和override关键字的动态调试图片,如下

080126577856689.gif

可以看到直接到子类去执行override关键字修饰的Say()方法。


那么如果父类使用virtual关键字修饰,而子类没有重写该方法时会怎么样呢?如下面的代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class  Program
     {
         static  void  Main( string [] args)
         {
                
             Person p1 =  new  Student();
             p1.Say();
             Console.ReadKey();
         }
     }
     class  Person
     {
         private  int  nAge;
         public  Person()
         {
             Console.WriteLine( "我是Person构造函数,我是一个人!" );
         }
         //这里定义了一个虚方法
         public  virtual  void  Say()
         {
             Console.WriteLine( "我是一个人!" );
         }
     }
     class  Student : Person
     {
         //子类中没有出现override关键字修饰的方法
         public  void  SayStude()
         {
             Console.WriteLine( "我是一个学生!" );
         }
     }


执行结果如下:


080132502632769.png



所以,如果子类找不到override方法,则会回溯到该子类的父类去找是否有override方法,知道回溯到自身的虚方法,并执行。

虚方法知识总结的思维导图如下:

080048580514226.png




     本文转自yisuowushinian 51CTO博客,原文链接:http://blog.51cto.com/yisuowushinian/1357174,如需转载请自行联系原作者




相关文章
|
20天前
|
存储 安全 编译器
学懂C#编程:属性(Property)的概念定义及使用详解
通过深入理解和使用C#的属性,可以编写更清晰、简洁和高效的代码,为开发高质量的应用程序奠定基础。
72 12
|
13天前
|
存储 监控 算法
企业内网监控系统中基于哈希表的 C# 算法解析
在企业内网监控系统中,哈希表作为一种高效的数据结构,能够快速处理大量网络连接和用户操作记录,确保网络安全与效率。通过C#代码示例展示了如何使用哈希表存储和管理用户的登录时间、访问IP及操作行为等信息,实现快速的查找、插入和删除操作。哈希表的应用显著提升了系统的实时性和准确性,尽管存在哈希冲突等问题,但通过合理设计哈希函数和冲突解决策略,可以确保系统稳定运行,为企业提供有力的安全保障。
|
2月前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
56 7
C# 9.0 新特性解析
|
1月前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
61 4
|
2月前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
44 2
|
2月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
64 1
|
2月前
|
消息中间件 存储 负载均衡
Apache Kafka核心概念解析:生产者、消费者与Broker
【10月更文挑战第24天】在数字化转型的大潮中,数据的实时处理能力成为了企业竞争力的重要组成部分。Apache Kafka 作为一款高性能的消息队列系统,在这一领域占据了重要地位。通过使用 Kafka,企业可以构建出高效的数据管道,实现数据的快速传输和处理。今天,我将从个人的角度出发,深入解析 Kafka 的三大核心组件——生产者、消费者与 Broker,希望能够帮助大家建立起对 Kafka 内部机制的基本理解。
98 2
|
3月前
|
存储 NoSQL MongoDB
MongoDB 概念解析
10月更文挑战第12天
51 0
MongoDB 概念解析
|
3月前
|
供应链 网络协议 数据安全/隐私保护
|
3月前
|
前端开发 JavaScript Shell
深入解析前端构建利器:webpack核心概念与基本功能全览
深入解析前端构建利器:webpack核心概念与基本功能全览—
38 0

推荐镜像

更多