c#如何跳出一个函数_C#-面向对象:被多少人误解的继承?

0f40b36285a9ad30594a207c7d79a113.png
叶飞:C#-面向对象-目录​zhuanlan.zhihu.com
b3df5b1304fc8c98a9c85c89ded1bb3b.png

面向对象三大特征:封装、继承和多态。

今天我们来学习继承。


中高级程序员可以直接拉到最后:继承的滥用


我们前面说过了,函数多了,所以我们把函数归类;那么代码进一步膨胀,类也多了的时候,我们就开始琢磨怎么对类进行分类了!

比如我们现在这么一些类:学生(Student)、老师(Teacher)、教室(Classroom)、寝室(bedroom)……经过观察,我们发现,学生和老师似乎可以归为“人(Person)”这一类,教室和寝室好像可以归为“房间(Rooom)”这一类,是不是?

C#为我们这样进行“归类”提供了语法:

继承

先上代码:

internal 

实现继承的语法很简单,关键就是冒号(:)。子类继承父类之后,就可以使用父类的成员:

new 

这就是继承最明显的特征。但这里仍然有一些注意事项:

首先,继承发生在类和类之间,而不是对象之间。或者说,子类继承的是父类的定义模板,而不是父类对象,父类对象的任何数据都不会传递给子类。让我们用代码来说明这个问题:

Person 

演示:

其次,一个父类可以有多个子类(记忆:可以有多个兄弟姐妹),但一个子类只能有一个父类(记忆:总是单亲),也就是说,C#不支持多重继承。

然后,子类还可以再作为其它子类的父类,也就是说,C#支持多层继承。比如,我们可以让“敏捷(学)生”再继承自之前的Student类:

internal 

这样,AgileStudent就拥有了Student和Person的成员定义:

AgileStudent 

最后,静态类不能被继承,但实例类中的静态成员一样可以被继承。

类的继承关系可以用类图 来表示,非常直观,类似于看图识字。比如上述类的继承关系就可以表示为:

a3b38db7b0666ef952993a74c3e20bc9.png

继承带来的另一个值得注意的问题,有关

构造函数

实例化一个子类,需要调用它 所有的(即包含祖先的) 父类构造函数。因为要能够使用继承自父类的成员,必须调用一次父类的构造函数(任何类都一样,你要使用这个类的实例成员,当然要先对这个类实例化一次——实例化就必须通过调用构造函数实现)

如果父类只有一个无参构造函数(隐式/显式的均可),.NET运行时会默认调用这个无参构造函数。但是,如果父类没有无参构造函数,就需要在子类的构造函数中指定具体调用父类的哪一个构造函数:

internal 

错误/过程断点演示:

此外,继承引入了新的访问修饰符:

protected(受保护的)

前面我们说:

子类可以使用父类的成员

其实并不准确,父类私有的(private)成员,子类就不能访问(也只有private的父类成员,子类不能访问)。但我们还可能有这样的需求:

父类的某个成员,除子类以外的其他地方都不能访问,或者说,只有在子类中才能访问。

这时候,就需要使用protected访问修饰符:

internal 

protected还可以和internal联合使用,当父类和子类不在同一个项目时有用。添加了protected的internal成员,可以被在另外一个项目中的子类使用。

还有作用于类的:

sealed(封闭的)

标记某个类不能再被继承,比如:

sealed 

演示:

有了继承之后,我们就可以学习C#一个很有意思的语法:

父类装子类

Person 

注意:

  • 等号左边,ywq被定义为Person类型,但是
  • 等号右边,实例化的是一个Student对象!

所以,实际上,这是一个父类变量引用(指向)了一个子类对象

从逻辑上理解,变量ywq被定义成了Person,Student也是Person,所以可以用这ywq指向Student对象,是不是这样?大家一定要理解这里的这个“是”字。

我们看反过来行不行:

Student 

ywq被定义为Student,new Person()一定学生?不一定。一个人可以是学生(Student),也可以是老师(Teacher)。如果是老师,学生ywq指向一个老师,逻辑上就不成立了。

逻辑以外,我们看代码实现:

Person 

变量能够调用(.出来)什么,是由声明这个变量类型决定的,而是变量所引用的对象类型决定的。

作为Person类型的变量,ywq能够调用Person的Name属性,这个属性也是其子类Student对象(因继承)可以使用的。

同时,父类变量ywq不能调用子类Student的属性Score,所以可以保证无论如何使用ywq变量,都不会出事。哪怕以后ywq指向其他对象,都没有问题:

ywq 

但是,假如子类变量可以引用父类对象:

Student 

你想一想,作为Person对象,成绩(Score)往哪里放啊?

同时,C#还专门为我们提供了一个运算符:

is(是)

来进行类型判断,直接上代码:

Person 

我们可以总结一下:

  • 类型判断是以运行时(即以所指向对象)为准
  • 子类对象可以是自己/父/祖先类型
  • 父类对象不能是子类类型

另外需要注意的是:

  • 如果变量和类型之间没有继承关系,结果必然为false;或者变量必然是该类型(比如值类型),结果必然为true,VS提示警告:
The given expression is always of the provided ('int') type
  • 如果变量值为null(没有对象引用),总是返回false
Person 

进行这样的判断有什么用呢?

我们前面在学习基本类型的时候,学习过强制类型转换。其实有继承关系的自定义类型也一样可以使用强制类型转换:

Person 

但是,有没有可能wx无法转换成Student呢?当然是有可能的,比如:

Person 

但强制类型转换不进行编译时检查(除非两个类型之间完全没有继承关系,没有任何转换的可能性),错误只会在运行时报出:

Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'YuanZhan.Person' to type 'YuanZhan.Student'

很多时候,我们不希望直接报错(这也就是要使用TryParse()方法替代Parse()的原因)!怎么办呢?我们可以先检查:能不能转换。怎么检查,就要靠is,比如:

if 

此外我们还有另一种方式,直接使用

as

进行转换。如果转换不成功,会返回null值。

Student 

as在底层实际上是也利用了is,但飞哥个人认为as的写法更“舒服”一点。同学们可以依据自己的喜好选择使用is或as。

在面向对象刚刚开始流行的时候,很多开发人员把继承视为一个可以实现“代码重用”的语法。结果导致了继承被大量的

滥用

基于“重用”的逻辑,当我们发现狗有四条腿,猫也有四条腿的时候,就会因为这“四条腿”进行抽象:

class 

看起来没什么问题,现在我们引入了一个新的桌子(Table)类,桌子也有四条腿,怎么办?难道让桌子也继承自动物(Animal)?怎么这么别扭呢?

通过你的仔细分析和思考,你想到了一个“好”办法,那就是再引入一个新类,叫什么名字呢?嗯,LegThing(四条腿的东西)吧:

class 

a1d8581c2e172d9117ec86c3e9c6337e.png

看起来还将就,除了这个LegThing名字别扭了一点。然而,好景不长,系统又引进了一个轿车类,它带了一个Run()的方法,这个Run()方法又恰好是Cat和Dog都有的,怎么办?三个Run()方法,重复,冗余!太难受了……

class 

于是你又想故技重施,引入了一个MoveThings类:

class 

8f68160444185e56dbfade638c547ed7.png

如果不考虑腿的事情,似乎还不错。但是,动物还要有腿啊,Animal已经继承了MoveThing,无法再继承LegThing。更要命的是:动物要腿,桌子一样要有腿,同学们可以好好想想,能够两全其美么?

你或者会说,这是C#的设计问题啊!为什么不允许多重继承呢?让Animal能够同时继承MoveThing和LegThing不就OK了么?

错!允许多重继承只会进一步的鼓励继承的滥用。

想一想,即使不出现上面的问题,顺着这种思路开发,随着类变得越来越多,你的继承层次就会变得越来越复杂越来越杂乱,这样Thing那样Thing的莫名其妙的类就会越来越多,直到代码结构变得无法理解,彻底瘫痪。

所以你可能会看到有文章说要避免继承的滥用,方法就是“控制继承的层次”,甚至指出最好3层不要超过5层之类的……这都是治标不治本的做法。其实关键的关键,你要明白:

继承 是为了 重用,而是为了 多态

其实,真正实现重用的:

  • 对行为而言,就是方法;
  • 对状态而言,应该是组合。即一个对象包含另外若干对象,或者若干对象组合成一个对象。

我们后面会学习“万物皆对象”,int/bool/string等各种变量值都是对象,所以任何一个对象,其实都是由若干对象组合而成的。以上面的代码为例,动物有腿就有腿,在Animal类里放上四条腿就OK了;桌子也有四条腿,就在Table类里也放上四条腿了。为啥要继承呢?就为了少写一行代码?但你多声明了一个类,多写了两个继承啊!

那些你会问:这样说来,我们就根本可以不用继承了嘛!都用组合不就OK了,为什么还要有Animal这个父类呢?Animal里放四条腿,给Dog和Cat重用,也不挺好的?

Good question!

首先,面向对象要映射现实。面向对象的根本目的是为了让代码更加容易被人理解,而现实是我们最容易理解的部分。合理的映射现实,有助于代码的理解。所以我们Cat和Dog继承自Animal更容易让人理解,而不是继承自什么LegThing,^_^

其次,继承本质上体现的是一种“”的关系。Cat和Dog继承自Animal,体现的是:猫和狗都“”动物。那为什么他们都是动物呢?更多的是因为他们共同/类似的行为(会跑会叫有生命),而不是属性(有一个脑袋四条腿),在我们面向对象的设计中,这一点至关重要。

最后,还是那句话:

继承是为了多态。

什么是多态?我们下一节课继续讲。

作业

  1. 让User类无法被继承
  2. 观察一起帮的求助(Problem)、文章(Article)和意见建议(Suggest),根据他们的特点,抽象出一个父类:内容(Content)
    1. Content中有一个字段:kind,记录内容的种类(problem/article/suggest等),只能被子类使用
    2. 确保每个Content对象都有kind的非空值
    3. Content中的createTime,不能被子类使用,但只读属性PublishTime使用它为外部提供内容的发布时间
    4. 其他方法和属性请自行考虑,尽量贴近一起帮的功能实现。
  3. 实例化文章和意见建议,调用他们:
    1. 继承自父类的属性和方法
    2. 自己的属性和方法
  4. 再为之前所有类(含User、HelpMoney等)抽象一个基类:Entity,包含一个只读的Id属性。试一试,Suggest能有Id属性么?

每日单词

709d22aad7dc1239465cafd4ab8a60a0.png

感谢童鞋们的阅读!^_^

我就是:黑律师/包工头/创业狗/老码农……现在还是教书匠的大飞哥。

再次重申这个系列的目标是:

1)通俗易懂。2)实战为主。3)面向就业。

系列内容的完善需要你的反馈!

欢迎点赞和评论,以及加入我们的QQ交流群:326801052。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值