C#-面向对象:多态

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

多态,是面向对象三大特征中最难以理解的一个。首先它的名字就非常难以理解,飞哥试着用自己的语言对它进行一个解释:

同一个变量,调用“同一个”的方法,可以呈现出多种形态。

怎么回事呢?我们首先来了解几个语法现象。


相同的方法

子类和父类可不可以有签名(名称、参数和返回值)完全相同的方法?这是可以的:

但是,我们会得到一个警告(warning),意思是说:子类Student的Eat()方法会隐藏(hide)父类Person的Eat()方法。如果这是我们有意而为之的(intented),让我们添加new关键字。所以,规范的写法应该是:

    internal class Person
    {
        internal void Eat()
        {
            Console.WriteLine("人吃饭");
        }
    }

    internal class Student : Person
    {
        internal new void Eat()   //注意关键字new
        {
            Console.WriteLine("学生吃饭");
        }  
    }
这样的话,Person对象就调用Person的Eat()方法,Student对象就调用Student的Eat()方法,互不相干:



            new Person().Eat();     //输出:人吃饭
            new Student().Eat();    //输出:学生吃饭

new还可以作用于静态方法。

但是,C#还有另外一个语法:父类装子类,就会造成一个问题:

            Person ywq = new Student();
            ywq.Eat(); 

ywq究竟该调用哪一个Eat()方法呢?究竟是依变量调用Person的,还是依对象调用Student的?

运行上述代码,你会发现,使用的是Person类的Eat()方法。这是因为我们在子类的方法中添加了一个new关键字。不知道你是怎么认为的,飞哥觉得这样怪怪的,其实大多数开发人员都这样觉得怪怪的,所以new关键字隐藏父类方法使用得非常少。在实际开发中,我们通常使用


重写(override)

重写需要首先在父类方法中添加virtual关键字,将其变成虚方法:

    internal class Person
    {
        internal virtual void Eat()     //注意关键字 virtual
        {
            Console.WriteLine("人吃饭");
        }
    }

    internal class Student : Person
    {
        internal override void Eat()   //注意关键字 override
        {
            Console.WriteLine("学生吃饭");
        }  
    }

然后再重复上述调用,就可以看出差别了:

            Person ywq = new Student();
            ywq.Eat();        //结果为:学生吃饭

语法到此为止,那多态如何体现呢?让我们再将ywq赋值给一个Person对象:


            Person ywq = new Student();
            ywq.Eat();     //代码第2行,输出:学生吃饭
            ywq = new Person();
            ywq.Eat();     //代码第4行,输出:人吃饭

对比代码第2行和第4行,同一个对象ywq,调用“同样的”方法Eat(),得到的却是不同的结果——这就是多态。请仔细体会,^_^

此外,子类可以使用base调用父类的方法。这在override虚方法中非常常见。

其次,可以在多重virtual和override。

    internal class Student : Person
    {
        internal override void Eat()   //这里已经是override了
        {
            Console.WriteLine("学生吃饭");
        }
    }

    internal class AgileStudent : Student
    {
        internal override void Eat()  //继续override
        {
            base.Eat();     //base仅指Student,不包含父类的父类
        }
    }

然后,可以在override方法上使用sealed关键字,标识override到此为止,不能再被子类override了。

演示:


那多态有什么用呢?我们举例来说明:源栈食堂为学生和老师都供应午餐,但如果是老师呢,就吃小灶;如果是学生呢,就吃大锅饭。你怎么实现供应午餐(ServeLunch)这个方法?是不是这样:

        static void ServeLunch(string role)
        {
            Console.WriteLine("开饭啦……");
            if (role == "老师")
            {
                new Teacher().Eat();
            }
            else if (role == "学生")
            {
                new Student().Eat();
            }
            //else ignore
            //其他代码……
        }

这样的代码问题很多:

  1. 没有必要引入一个string role,
  2. 更不应该在方法中new Teacher或者Student,调用一次ServeLunch()就要生成一个新的老师或学生?难道不应该是为某个老师或学生提供午餐么?
  3. 要小心if...else...,在实际的开发中,复杂的if...else...嵌套是代码维护的噩梦,能够少一点就少一点。

怎么优化呢?这就要利用到多态了:

        static void ServeLunch(Person person)
        {
            Console.WriteLine("开饭啦……");
            person.Eat();   //不同的Person对象自然会调用不同的方法
            //其他代码……
        }

咦?!首先if...else...没了,需求中的“如果...就……”能实现么?试一试呗!^_^


但减少if...else只是多态应用的结果,还不是应用多态的关键。多态为我们编程开发,带来的最大的好处,是让我们抽象的(面向对象的)设计实现成为可能

飞哥个人认为,面向对象其实应该是两大特征:封装和多态。继承可以算一个小特征,它只是为多态提供基础而已,所以我们说

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

面向对象(Object Oriented)中的“面向(Oriented)”,和“面向未来”“面向现代化”里的面向类似,指的是在项目的设计和开发(甚至于测试)中,都应该“以对象为核心以对象为导向”进行。

和面向对象相对应(但不是对立)的是“面向过程”。他们的区别是什么?拿到一个需求,如果你:

  • 面向过程,那你考虑的就是如何一步一步的完成,分支循环之类的东西;
  • 面向对象,那你考虑的就是这事由哪个或者哪几个对象来做,如果是几个对象,他们之间如何协作……

这就是面向对象,不考虑具体的实现细节,而是考虑对象的组织调用。

我们一直在说,面向对象的核心是代码的组织管理,就是这个意思。假如你是一个公司的管理者,你如何管理公司?是不是财务的事情交给财务人员去做,销售的事情交给销售人员去做?你会不会考虑如何记账收钱,如何写文案发软文?只要企业规模一上来,你这样做就是不行的。面向对象的编程也一样,代码规模一上来,你还去思考每一个方法的实现细节是不行的。

你可能会说,但方法实现还是要有啊!不然功能实现怎么办?对的,所以,我们才说面向对象对应的是面向过程,他们不说对立的,不是说面向对象了就不能再面向过程了,恰恰相反:面向过程,始终是面向对象的基础。

但仅有对象还是不够的,对象还需要被抽象(继承和多态),否则大量的类一样会把我们搞得懵逼。有了继承和多态,我们就可以基于数量较少的若干基类进行更简化的思考。就像上面的例子,我们把学生和老师都抽象成“人”,是人就吃饭,至于他们各自具体吃什么,我们在进行“全局思考”时不关心。

——这可能就是初学者最不放心的地方:怎么能不关心呢?!不关心代码功能实现不了怎么办呢?哈哈,经验不够没信心,这就没办法了哟……

还是那句话:

有些东西,是要靠时间来堆的!

所以,“程序猿35岁过后就没用了”,扯淡呢!“键盘敲烂,月薪过万”,同学们还是可劲的做作业吧,^_^


作业:

添加一个新类ContentService,其中有一个发布(Release())方法:

  1. 如果发布Article,需要Article作者消耗一个帮帮币
  2. 如果发布Problem,需要消耗Problem作者其设置悬赏数量的帮帮币
  3. 如果发布Suggest,不需要消耗帮帮币

最后将内容存到数据库中,三个类存数据库的方法是完全一样的,现在用Console.WriteLine("saved into db")代替。

根据我们学习的继承和多态知识,实现上述功能。


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

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 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

分层架构和综合实战

全部
关键字



帮助

反馈