我们前面说过了,函数多了,所以我们把函数归类;那么代码进一步膨胀,类也多了的时候,我们就开始琢磨怎么对类进行分类了!
比如我们现在这么一些类:学生(Student)、老师(Teacher)、教室(Classroom)、寝室(bedroom)……经过观察,我们发现,学生和老师似乎可以归为“人(Person)”这一类,教室和寝室好像可以归为“房间(Rooom)”这一类,是不是?
这样“对类再进行归类”的语法支持就是:
把具有相同/类似特征的类再抽象成一个基类(base)/父类(parent),而剩下的作为派生类(derived)/子类(child)通过继承他们,拥有定义在基类/父类中的类成员。
先上代码:
class Person //被继承的类被称之为父类(或者基类) { public string Name; //使用字段略有不规范,应使用属性,此处为了兼顾Java public void eat() { Console.WriteLine("吃饭去了……"); } } class Student //子类(或者派生类) : Person //在类名后面添加冒号(:)和要继承的类名 { } class Teacher : Person { }
实现继承的语法很简单,关键就是冒号(:)(Java和JavaScript中使用关键字extends代替)
子类继承父类之后,就可以使用父类的成员:
new Student().eat(); //注意Student类中没有内容 new Teacher().Name="飞哥"; //注意Teacher类中没有内容
这就是继承最明显的特征。
语义理解:继承(inherit)了革命先辈的光荣传统(先辈有的我也有)
阅读下面的代码,@想一想@:子类对象yfei的Name值是什么?
Person fg = new Person(); fg.Name= "叶飞"; //给父类对象fg的Name赋值 Teacher yfei = new Teacher(); //实例化一个子类 //子类yfei的Name不是父类对象fg的Name“叶飞” Console.WriteLine(yfei.Name);
演示:yfei没能“继承”到父类对象fg的Name值
这是因为:继承是发生在类和类之间,而不是对象之间的。或者说,子类继承的是父类的定义模板,而不是父类对象,父类对象的任何数据都不会传递给子类。
静态成员一样可以被继承。
演示:把Name变成静态的:
public static string Name;然后用父类赋值:
Person.Name = "人";@想一想@:输出结果是?
Console.WriteLine(Teacher.Name);最后,在继承里面,方法要看出是一种声明,和字段的声明是一样的,声明就是属于类,是类模板的一部分。
其次,一个父类可以有多个子类(记忆:可以有多个兄弟姐妹),但一个子类只能有一个父类(记忆:总是单亲),也就是说,C#不支持多重继承。
然后,子类还可以再作为其它子类的父类,也就是说,C#支持多层(不是“多重”)继承。
比如,我们可以让“敏捷(学)生”再继承自之前的Student类:
class Person //祖先类 { public string Name; } class Student //父类 : Person { public int Score; } class AgileStudent //子类 : Student { }这样,AgileStudent就拥有了Student和Person的成员定义:
AgileStudent ljp = new AgileStudent(); ljp.Name = "刘江平"; //Name继承自Person ljp.Score = 85; //Score继承自Student
类的继承关系可以用类图来表示,非常直观,类似于看图识字。比如上述类的继承关系就可以表示为:
PS:类图是UML的一种,UML已经无可救药的衰落了,类图是仅存仍然有用的几种图形之一
前面我们说:子类可以使用父类的成员,其实并不准确:
父类私有的(private)成员,子类就不能访问(也只有private的父类成员,子类不能访问)。
但我们还可能有这样的需求:父类的某个成员,子类可以访问,外部不能访问。
这时候,就需要使用protected(受保护的)访问修饰符:
class Person { protected Person(string name) { } private string Name { get; set; } }但所谓的“外部”,Java和C#略有不同。
有了继承之后,我们就可以学习C#一个很有意思的语法:
Person atai //ywq被定义为Person类型 = new Student(); //实例化了一个Student对象
注意:
所以,实际上,这是一个父类变量引用(指向)了一个子类对象。
PS:JavaScript弱类型语言,不需要这个知识点。我们看反过来行不行:
Student atai = new Person(); //会有编译错误
这个语法点:要么硬记,开始记不住也没关系,有IDE的智能提示。可以用“大鞋装小脚”来帮助记忆。大,是指概念更大(人的概念就比学生更大)。
或者理解
Student atai = new Person(); //假设这样可以 atai.Score = 85; //Score是学生独有的,Person对象哪有这个字段?
父类变量装子类对象,实际上是一种隐式(自动)转换。(演示:当赋值无法进行时的错误提示)
既然有隐式,那就有显式(强制)转换。
有继承关系的自定义类型之间可以使用强制类型转换:
Person wx = new Student(); //Console.WriteLine(wx.Score); //报错:Score不是Person的属性 //但可以将wx强制转换成Student后当做Student对象使用 Console.WriteLine(((Student)wx).Score);
对比:基本(值)类型间的隐式/显式转换
但是,有没有可能wx无法转换成Student呢?当然是有可能的,编译时只检查对象变量和类之间有无继承关系,所以比如:
Person pzq = new Person(); //pzq是一个Person对象,无法转换成Student Console.WriteLine(((Student)pzq).Score);
错误只会在运行时报出:
Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'YuanZhan.Person' to type 'YuanZhan.Student'
很多时候,我们不希望直接报错(运行时错误会导致程序中断,性能下降等)!怎么办呢?我们可以先检查:能不能转换。
类似这样的代码:
if (/* pzq 是 Student 的实例 */) { Console.WriteLine(((Student)pzq).Score); }
C#中使用is(是不是),Java中使用instanceof(…的实例)来进行类型判断检查。
直接上代码:
Person wx = new Person(); Console.WriteLine(wx is Person); //true:是自己的类型 Console.WriteLine(wx is Student); //false:不是子类型 Student pzq = new Student(); Console.WriteLine(pzq is Student); //true: Console.WriteLine(pzq is Person); //true:也是自己的父类型 //class OnlineStudent : Student 线上学生是学生的子类 OnlineStudent xp = new OnlineStudent(); Console.WriteLine(xp is Student); //true:是自己的父类型 Console.WriteLine(xp is Person); //true:也是自己的祖先类型 wx = xp; Console.WriteLine(wx is Person); //true: Console.WriteLine(wx is Student); //true: Console.WriteLine(wx is OnlineStudent); //true:以运行时的类型为准
我们可以总结一下:
另外需要注意的是:
Person wx = null; Console.WriteLine(wx is Person); //false:wx是null值
基于“重用”的逻辑,当我们发现狗有四条腿,猫也有四条腿的时候,就会因为这“四条腿”进行抽象:
class Animal { int Legs { get; set; } } class Cat : Animal{} class Dog : Animal{}
看起来没什么问题,现在我们引入了一个新的桌子(Table)类,桌子也有四条腿,怎么办?难道让桌子也继承自动物(Animal)?怎么这么别扭呢?
通过你的仔细分析和思考,你想到了一个“好”办法,那就是再引入一个新类,叫什么名字呢?嗯,LegThing(四条腿的东西)吧:
class LegsThing { int Legs { get; set; } } class Animal : LegsThing{ } class Table : LegsThing { } class Cat : Animal { } class Dog : Animal { }
看起来还将就,除了这个LegThing名字别扭了一点。然而,好景不长,系统又引进了一个轿车类,它带了一个Run()的方法,这个Run()方法又恰好是Cat和Dog都有的,怎么办?三个Run()方法,重复,冗余!太难受了……
class Cat : Animal { void Run() { } } class Dog : Animal { void Run() { } } class Car { void Run() { } }
于是你又想故技重施,引入了一个MoveThings类:
class MoveThing { void Run() { } } class Animal : MoveThing { } class Cat : Animal { } class Dog : Animal { } class Car : MoveThing { }
如果不考虑腿的事情,似乎还不错。但是,动物还要有腿啊,Animal已经继承了MoveThing,无法再继承LegThing。更要命的是:动物要腿,桌子一样要有腿,同学们可以好好想想,能够两全其美么?
你或者会说,这是C#的设计问题啊!为什么不允许多重继承呢?让Animal能够同时继承MoveThing和LegThing不就OK了么?
错!允许多重继承只会进一步的鼓励继承的滥用。
想一想,即使不出现上面的问题,顺着这种思路开发,随着类变得越来越多,你的继承层次就会变得越来越复杂越来越杂乱,这样Thing那样Thing的莫名其妙的类就会越来越多,直到代码结构变得无法理解,彻底瘫痪。
所以你可能会看到有文章说要避免继承的滥用,方法就是“控制继承的层次”,甚至指出最好3层不要超过5层之类的……这都是治标不治本的做法。
其实关键的关键,你要明白:
继承不是为了重用,而是为了多态。
其实,真正实现重用的:
我们后面会学习“万物皆对象”,int/bool/string等各种变量值都是对象,所以任何一个对象,其实都是由若干对象组合而成的。以上面的代码为例,动物有腿就有腿,在Animal类里放上四条腿就OK了;桌子也有四条腿,就在Table类里也放上四条腿了。为啥要继承呢?就为了少写一行代码?但你多声明了一个类,多写了两个继承啊!
多快好省!前端后端,线上线下,名师精讲
更多了解 加: