C#-面向对象:继承

更多
2020年10月22日 09点24分 作者:叶飞 修改

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

今天我们来学习继承。

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

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

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

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

继承

先上代码:


  internal class Person    //被继承的类被称之为父类(或者基类)
    {
        internal string Name { get; set; }
        internal void eat()
        {
            Console.WriteLine("吃饭去了……");
        }
    }
    
    internal class Student    //子类(或者派生类)
        : Person       //在类名后面添加冒号(:)和要继承的类名
    {
    }

    internal class Teacher : Person { }

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


new Student().eat();                   //注意Student类中没有内容 
new Teacher().Name="飞哥";             //注意Teacher类中没有内容

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

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

Person fg = new Person();
fg.Name = "叶飞";                //给父类对象fg的Name赋值
Teacher yfei = new Teacher();    //实例化一个子类
//子类yfei的Name不是父类对象fg的Name“叶飞”
Console.WriteLine(yfei.Name);    



演示:

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

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

internal class Person    //祖先类
{
    internal string Name { get; set; }
}

internal class Student   //父类
    : Person
{
    internal int Score { get; set; }
}

internal class AgileStudent  //子类
    : Student
{ }
这样,AgileStudent就拥有了Student和Person的成员定义:
AgileStudent ljp = new AgileStudent();
ljp.Name = "刘江平";    //Name继承自Person
ljp.Score = 85;         //Score继承自Student

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

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


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

构造函数

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

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


internal class Person
{
    //父类没有无参构造函数了
    public Person(string name) { }
    internal string Name { get; set; }
}

internal class Student : Person
{
    //子类必须显式地指明调用父类的某一个构造函数
    public Student(string name)
        //使用base关键字,将子类实例化获得的name传递给父类
        : base(name)
    //: base("飞哥")    //也可以传一个固定值 
    { }
}

错误/过程断点演示:


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

protected(受保护的)

前面我们说:

子类可以使用父类的成员

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

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

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


internal class Person
{
    protected Person(string name) { }
    private string Name { get; set; }
}

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

还有作用于类的:


sealed(封闭的)

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


sealed class Student : Person {}

演示:


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

父类装子类

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

注意:

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

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

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

我们看反过来行不行:




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

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

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

            Person ywq = new Student();
            ywq.Name = "于维谦";       //OK
            ywq.Score                  //报错

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

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

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

    ywq = new Person(); //或:
            ywq = new Teacher();

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


            Student ywq = new Person();   //假设这样可以
            ywq.Score = 85;               //Score是学生独有的



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


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

is(是)

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

            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:以运行时的类型为准

我们可以总结一下:

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

另外需要注意的是:

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


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

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

            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'

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

            if (pzq is Student)
            {
                Console.WriteLine(((Student)pzq).Score);
            } 


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

as

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

            Student converted = pzq as Student;
            if (converted != null)
            {
                Console.WriteLine(converted.Score);
            }

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


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

滥用

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

    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类里也放上四条腿了。为啥要继承呢?就为了少写一行代码?但你多声明了一个类,多写了两个继承啊!

那些你会问:这样说来,我们就根本可以不用继承了嘛!都用组合不就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属性么?






源栈培训 C# 语法 基础 对象
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

后台开发


ADO和EF

如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……

其他:WebForm和WebApi

其他ASP.NET框架,如WebForm、WebApi……

RazorPages(Core)

微软推荐的、最新的、基于Razor页面和.NET core的新一代Web项目开发技术,包括Razor Tag Helper、Model绑定和Validation、Session/Cookie、内置依赖注入等……

MVC(Framework)

过去两年间最流行的、基于.NET Framework和MVC模式的ASP.NET MVC框架,主要用于讲解安全、性能、架构和各种实战功能演示……

C#语法

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

Java语法

面向过程的变量赋值、分支循环和函数封装;面向对象的封装、继承和多态;以及更高阶的常用类库(集合/IO/多线程……)、lambda等

Java Web开发

SpringMVC

分层架构和综合实战

全部
关键字



帮助

反馈