大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: 编程语言 修改讲义


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

比如我们现在这么一些类:学生(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已经无可救药的衰落了,类图是仅存仍然有用的几种图形之一


protected

前面我们说:子类可以使用父类的成员,其实并不准确:

父类私有的(private)成员,子类就不能访问(也只有private的父类成员,子类不能访问)。

但我们还可能有这样的需求:父类的某个成员,子类可以访问,外部不能访问。

这时候,就需要使用protected(受保护的)访问修饰符:

class Person
{
    protected Person(string name) { }
    private string Name { get; set; }
}
但所谓的“外部”,Java和C#略有不同。



父类装子类

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

 Person atai            //ywq被定义为Person类型
 = new Student();  //实例化了一个Student对象

注意:

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

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

PS:JavaScript弱类型语言,不需要这个知识点。

我们看反过来行不行:

    Student atai = new Person();  //会有编译错误

这个语法点:要么硬记,开始记不住也没关系,有IDE的智能提示。可以用“大鞋装小脚”来帮助记忆。大,是指概念更大(人的概念就比学生更大)。

或者理解

  • 从逻辑上讲,变量atai被定义成了Person,Student也Person,所以可以用这atai指向Student对象;ywq被定义为Student,new Person()一定学生?不一定。一个人可以是学生(Student),也可以是老师(Teacher)。如果是老师,学生ywq指向一个老师,逻辑上就不成立了。大家一定要理解这里的这个“是”字。
  • 逻辑以外,首先牢记:变量能够调用(.出来)什么,是由声明这个变量类型决定的,而不是变量所引用的对象类型决定的。
    • 父装子的时候,作为Person类型的变量,atai能够调用Person的Name属性,这个属性也是其子类Student对象(因继承)可以使用的;同时,父类变量ywq不能调用子类Student的属性Score,所以可以保证无论如何使用ywq变量,都不会出事。
    • 假如子类变量可以引用父类对象:
      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:以运行时的类型为准

我们可以总结一下:

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

另外需要注意的是:

  • 如果变量和类型之间没有继承关系,就没有任何转换的可能性:C#会警告,Java会报错
  • 如果变量值为null(没有对象引用),总是返回false
    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类里也放上四条腿了。为啥要继承呢?就为了少写一行代码?但你多声明了一个类,多写了两个继承啊!


作业:

  1. 观察一起帮的求助(Problem)、文章(Article)意见建议(Suggest),根据他们的特点,抽象出一个父类:内容(Content)
    1. Content中有一个字段:kind,记录内容的种类(problem/article/suggest等),只能被子类使用
    2. Content中的createTime,不能被子类使用,但只读属性PublishTime使用它为外部提供内容的发布时间
    3. 其他方法和属性请自行考虑,尽量贴近一起帮的功能实现。
  2. 再为之前所有类(含User、HelpMoney等)抽象一个基类:Entity,包含一个只读的Id属性。
  3. 实例化上述所有类,调用他们:
    1. 继承自父类的属性和方法
    2. 自己的属性和方法
学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 编程语言 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码