面向对象

思考并回答以下问题:

  • 继承是有缺点的,那就是父类变,则子类不得不变。什么意思?
  • 如果两个对象A和B,可以描述为‘B是A’,则表明B可以继承A。怎么理解?

类与实例

“现在我们可以讲面向对象的三大特性之一‘封装’了。每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。这样方法和属性包装在类中,通过类的实例来实现。”

对象的继承代表了一种‘is-a’的关系,如果两个对象A和B,可以描述为‘B是A’,则表明B可以继承A。‘猫是哺乳动物’,就说明了猫与哺乳动物之间继承与被继承的关系。实际上,继承者还可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。例如,猫就可能拥有抓老鼠、爬树等‘哺乳动物’对象所不具备的属性。因而,在继承关系中,继承者可以完全替换被继承者,反之则不成立。所以,我们在描述继承的‘is-a’关系时,是不能相互颠倒的。说‘哺乳动物是猫’显然有些莫名其妙。继承定义了类如何相互关联,共享特性。继承的工作方式是,定义父类和子类,或叫做基类和派生类,其中子类继承父类的所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。

“学习继承最好是记住三句话,如果子类继承于父类,第一、子类拥有父类非private的属性和功能;第二、子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;第三、子类还可以以自己的方式实现父类的功能(方法重写)。”

继承是有缺点的,那就是父类变,则子类不得不变。让狗去继承于猫,显然不是什么好的设计,另外,继承会破坏包装,父类实现细节暴露给子类,这其实是增大了两个类之间的耦合性。

两个类尽管分开,但如果关系密切,一方的变化都会影响到另一方,这就是耦合性高的表现,继承显然是一种类与类之间强耦合的关系。

当两个类之间具备‘is-a’的关系时,就可以考虑用继承了,因为这表示一个类是另一个类的特殊种类,而当两个类之间是‘has-a’的关系时,表示某个角色具有某一项责任,此时不合适用继承。比如人有两只手,手不能继承人;再比如飞机场有飞机,这飞机也不能去继承飞机场。

A.8 多态

多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。看定义显然不太明白,我来举个例子你就好理解了。我们的国粹‘京剧’以前都是子承父业,代代相传的艺术。假设有这样一对父子,父亲是非常有名的京剧艺术家,儿子长大成人,模仿父亲的戏也惟妙惟肖。有一天,父亲突然发高烧,上不了台表演,而票都早就卖出,退票显然会大大影响声誉。怎么办呢?由于京戏都是需要化妆才可以上台的,于是就决定让儿子代父亲上台表演。

这里面有几点注意,第一,子类以父类的身份出现,儿子代表老子表演,化妆后就是以父亲身份出现了。第二、子类在工作时以自己的方式来实现,儿子模仿得再好,那也是模仿,儿子只能用自己理解的表现方式去模仿父亲的作品;第三、子类以父类的身份出现时,子类特有的属性和方法不可以使用,儿子经过多年学习,其实已经有了自己的创作,自己的绝活,但在此时,代表父亲表演时,绝活是不能表现出来的。当然,如果父亲还有别的儿子会表演,也可以在此时代表父亲上场,道理也是一样的。这就是多态。

我们还需要了解一些概念,虚方法和方法重写。为了使子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的。这是通过在该成员的返回类型之前添加virtual关键字来实现。通常虚拟的是方法,但其实除了字段不能是虚拟的,属性、事件和索引器都可以是虚拟的。尽管方法可以是虚拟的,但虚方法还是有方法体,可以实际做些事情。然后,子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写Override,或者叫做方法覆写。

由于Cat和Dog都有Shout的方法,只是叫的声音不同,所以我们可以让Animal有一个Shout的虚方法,然后Cat和Dog去重写这个Shout,用的时候,就可以用猫或狗代替Animal叫唤,来达到多态的目的。

Animal相当于京剧表演的老爸,Cat和Dog相当于儿子,儿子代表父亲表演Shout,但Cat叫出来的是‘喵’,Dog叫出来的是‘汪’,这就是所谓的不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。

这个对象的声明必须是父类,而不是子类,实例化的对象是子类,这才能实现多态。多态的原理是当方法被调用时,无论对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用。也就是说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的。

多态被称为面向对象三大特性,我感觉不到它有和封装、继承同样的作用。”

要深刻理解并会合理利用多态,不去研究设计模式是很难做到的。也可以反过来说,没有学过设计模式,那么对多态、乃至对面向对象的理解多半都是肤浅和片面的。

A.9 重构

“嗨,你把继承的基本忘记了?继承第一条是什么?”

哈,是子类拥有所有父类非private的属性和方法。对的对的,由于子类继承父类,所以是public的Shout方法是一定可以为所有子类所用的。”史熙高兴地说,“我渐渐能感受到面向对象编程的魅力了,的确是非常的简捷。由于不重复,所以需求的更改都不会影响到其他类。

“这里其实就是在用一个设计模式,叫模板方法。

A.10 抽象类

Animal类其实根本就不可能实例化的,你想呀,说一只猫长什么样,可以想象,说new Animal(); 即实例化一个动物。一个动物长什么样?”

动物是一个抽象的名词,没有具体对象与之对应。所以我们完全可以考虑把实例化没有任何意义的父类,改成抽象类,同样的,对于Animal类的getShoutSound方法,其实方法体没有任何意义,所以可以将virtual修饰符改为abstract,使之成为抽象方法。C#允许把类和方法声明为abstract,即抽象类和抽象方法。

这样一来,Animal就成了抽象类了。抽象类需要注意几点,第一,抽象类不能实例化,刚才就说过,‘动物’实例化是没有意义的;第二,抽象方法是必须被子类重写的方法,不重写的话,它的存在又有什么意义呢?其实抽象方法可以被看成是没有实现体的虚方法;第三,如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。

一开始就可以把Animal类设成抽象类了,根本没有必要存在虚方法的父类。我们应该考虑让抽象类拥有尽可能多的共同代码,拥有尽可能少的数据。

抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类[。也就是说,具体类不是用来继承的。我们作为编程设计者,应该要努力做到这一点。比如,若猫、狗、牛、羊是最后一级,那么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的。

A.11 接口

“在动物运动会里还有一项非常特殊的比赛是为了给有特异功能的动物展示其特殊才能的。”

“哈,有特异功能?有意思。不知是什么动物?”

“多的是呀,可以来比赛的比如有机器猫叮,石猴孙悟空,肥猪猪八戒,再比如蜘蛛人、蝙蝠侠等。”

“啊,这都是什么动物呀,根本就是人们虚构之物。”

“让猫狗比赛叫声难道就不是虚构?你当它们会愿意相互攀比?其实我的目的只是为了让两个动物尽量的不相干而已。现在叮会从肚皮的口袋里变出东西,而孙悟空可以拔根毫毛变出东西,且有七十二般变化,八戒有三十六般变化。它们各属于猫、猴、猪,现在需要让它们比赛谁变东西的本领大。你来分析一下如何做?”

“‘变出东西’应该是叮、孙悟空、猪八戒的行为方法,要想用多态,就得让猫、猴、猪有‘变出东西’的能力,而为了更具有普遍意义,干脆让动物具有‘变出东西’的行为,这样就可以使用多态了。”

“哈哈,史熙呀,你犯了几乎所有学面向对象的人都会犯的错误,‘变出东西’它是动物的方法吗?如果是,那是不是所有的动物都必须具备‘变出东西’的能力呢?”

“这个,确实不是,这其实只是三个特殊动物具备的方法。那应该如何做?”

“这时候我们就需要新的知识了,那就是接口interface。接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。所以接口不能实例化,不能有构造方法和字段;不能有修饰符,比如public、private等;不能声明虚拟的或静态的等。还有实现接口的类就必须要实现接口中的所有方法和属性。”

“怎么接口这么麻烦。”

“要求是多了点,但一个类可以支持多个接口,多个类也可以支持相同的接口。所以接口的概念可以让用户和其他开发人员更容易理解其他人的代码。哦,对了,记住,接口的命名,前面要加一个大写字母‘I’,这是规范。”

“听不懂呀,不如讲讲实例吧。”

“我们先创建一个接口,它是用来‘变东西’用的。注意接口用interface声明,而不是class,接口名称前要加‘I’,接口中的方法或属性前面不能有修饰符、方法没有方法体。”

“然后我们来创建机器猫的类。”

“猴子的类Monkey和孙悟空的类StoneMonkey与上面非常类似,在此省略。此时我们的客户端,可以加一个‘变出东西’按钮,并实现下面的代码。”

运行结果

“哦,我明白了,其实这和抽象类是很类似的,由于我现在要让两个完全不相干的对象,叮和孙悟空来做同样的事情‘变出东西’,所以我不得不让他们去实现做这件‘变出东西’的接口,这样的话,当我调用接口的‘变出东西’的方法时,程序就会根据我实现接口的对象来做出反应,如果是叮,就是用万能口袋,如果是孙悟空,就是七十二变,利用了多态性完成了两个不同的对象本不可能完成的 任务。”

“说得非常好,同样是飞,鸟用翅膀飞,飞机用引擎加机翼飞,而超人呢?举起两手,握紧拳头就能飞,它们是完全不同的对象,但是,如果硬要把它们放在一起的话,用一飞行行为的接口,比如命名为IFly的接口来处理就是非常好的办法。”

“但是我对抽象类和接口的区别还是不太清楚。”

“问到点子上了,这两个概念的异同点是网上讨论面向对象问题时讨论得最多的话题之一,从表象上来说,抽象类可以给出一些成员的实现,接口却不包含成员的实现,抽象类的抽象成员可被子类部分实现,接口的成员需要实现类完全实现,一个类只能继承一个抽象类,但可实现多个接口等等。但这些都是从两者的形态上去区分的。我觉得还有三点是能帮助我们去区分抽象类和接口的。第一,类是对对象的抽象;抽象类是对类的抽象;接口是对行为的抽象。接口是对类的局部(行为)进行的抽象,而抽象类是对类整体(字段、属性、方法)的抽象。如果只关注行为抽象,那么也可以认为接口就是抽象类。总之,不论是接口、抽象类、类甚至对象,都是在不同层次、不同角度进行抽象的结果,它们的共性就是抽象。第二,如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。比如猫呀狗呀它们其实都是动物,它们之间有很多相似的地方,所以我们应该让它们去继承动物这个抽象类,而飞机、麻雀、超人是完全不相关的类,叮是动漫角色,孙悟空是古代神话人物,这也是不相关的类,但它们又是有共同点的,前三个都会飞,而后两个都会变出东西,所以此时让它们去实现相同的接口来达到我们的设计目的就很合适了。”

“哦,明白,其实实现接口和继承抽象类并不冲突的,我完全可以让超人继承人类,再实现飞行接口,是吗?”

“对,超人除了内裤外穿以外,基本就是一个正常人的样子,让他继承人类是对的,但他本事很大,除了飞天,他还具有刀枪不入、力大无穷等等非常人的能力,而这些能力也可能是其他对象具备的,所以就让超人去实现飞行、力大无穷等行为接口,这就可以让超人和飞机比飞行,和大象比力气了,这就是一个类只能继承一个抽象类,却可以实现多个接口的做法。”

“那还有一点呢?”

“嗯,这一点更加关键,那就是第三,从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确认,预先定义。这里其实说明的是抽象类和接口设计的思维过程。回想一下我们今天刚开始讲的时候,先是有一个Cat类,然后再有一个Dog类,观察后发现它们有类似之处,于是泛化出Animal类,这也体现了敏捷开发的思想,通过重构改善既有代码的设计。事实上,只有小猫的时候,你就去设计一个动物类,这就极有可能会成为过度设计了。所以说抽象类往往都是通过重构得来的,当然,如果你事先意识到多种分类的可能,那么事先就设计出抽象类也是完全可以的。而接口就完全不是一回事,比如我们是动物运动会的主办方,在策划时,大家就坐在一起考虑需要组织什么样的比赛,大家商议后,觉得应该设置如跑得最快、跳得最高、飞得最远、叫得最响、力气最大等等比赛项目,而此时,主办方其实还不太清楚会有什么样的动物来参加运动会,所有的这些比赛项目都可能是完全不相同的动物在比,它们将如何去实现这些行为也不得而知,此时,能做的事就是事先定义这些比赛项目的行为接口。”

“啊,你的意思是不是说,抽象类是自底而上抽象出来的,而接口则是自顶向下设计出来的。”

“对,可以这么说。其实仅仅理解这一点是不够的,要想真正把抽象类和接口用好,还是需要好好用心地去学习设计模式。只有真正把设计模式理解好了,那么你才能算是真正会合理应用抽象类和接口了。”

A.12 集合

“下面我们再来看看,客户端的代码中,‘动物报名’用的是Animal类的对象数组,你设置了数组的长度为5,也就是说最多只能有五个动物可以报名参加‘叫声比赛’,多了就不行了。这显然是非常不合理的,应该考虑改进。你能说说数组的优缺点吗?”

“数组优点,比如说数组在内存中连续存储,因此可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。缺点嘛,应该是创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难。”

“说得不错,的确是这样,这就可能使得数组长度设置过大,造成内存空间浪费,长度设置过小造成溢出。所以.NET Framework提供了用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。大多数集合类实现相同的接口。我们现在介绍当中最常用的一种,ArrayList。”

“集合?它和数组有什么区别?”

“别急,首先ArrayList是命名空间System.Collections下的一部分,它是使用大小可按需动态增加的数组实现IList接口[MSDN]。”

“哦,没学接口前不太懂,现在知道了,你的意思是说,IList接口定义了很多集合用的方法,ArrayList对这些方法做了具体的实现?”

“对的,ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动增加。使用整数索引可以访问此集合中的元素。此集合中的索引从零开始。[MSDN]”

“是不是可以这样理解,数组的容量是固定的,而 ArrayList的容量可根据需要自动扩充。”

“是的,由于实现了IList,所以ArrayList提供添加、插入或移除某一范围元素的方法。下面我们来看看如何做。”

“对于‘叫声比赛’的代码,没有什么变化。”

“如果有动物报完名后,由于某种原因(比如政治、宗教、兴奋剂、健康等等)放弃比赛,此时应该需要将其从名单中移除。例如,在报了名后,两只小狗需要退出比赛。我们查了一下它们的报名索引次序为1和2(从0开始计算),所以我们可以应用集合的RemoveAt方法,它的作用是移除指定索引处的集合项。”

“我明白怎么做了。”

“哈,你太着急,集合与数组的不同就在于此,程序在执行RemoveAt(1)的时候,也就是叫‘阿毛’的Dog被移除了集合,此时‘小黑’的索引次序还是原来的2吗?”

“哦,我明白了,等于整个后序对象都向前移一位了。应该是这样才对。也就是说,集合的变化是影响全局的,它始终都保证元素的连续性。”

arrayAnimal.RemoveAt(1);
arrayAnimal.RemoveAt(1);
“总结一下,集合ArrayList相比数组有什么好处?”

“主要就是它可以根据使用大小按需动态增加,不用受事先设置其大小的控制。还有就是可以随意地添加、插入或移除某一范围元素,比数组要方便。”

“对,这是ArrayList的优势,但它也有不足,ArrayList不管你是什么对象都是接受的,因为在它眼里所有元素都是Object,这就使得如果你‘arrayAnimal.Add(123);’或者‘arrayAnimal.Add(“HelloWorld”);’在编译时都是没有问题的,但在执行时,‘foreach(Animal item in arrayAnimal)’需要明确集合中的元素是Animal类型,而123是整型,HelloWorld是字符串型,这就会在运行到此处时报错,显然,这是典型的类型不匹配错误,换句话说,ArrayList不是类型安全的。还有就是ArrayList对于存放值类型的数据,比如int、string型(string是一种拥有值类型特点的特殊引用类型)或者结构struct的数据,用ArrayList就意味着都需要将值类型装箱为Object对象,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗。”

“等等,我不太懂,装箱和拆箱是什么意思?”

“所谓装箱就是把值类型打包到Object引用类型的一个实例中。比如整型变量i被“装箱”并赋值给对象o。”

int i = 123;
object o =(object) i; // boxing
“所谓拆箱就是指从对象中提取值类型。此例中对象o拆箱并将其赋值给整型变量i”

o = 123;
i =(int) o; // unboxing
“相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制转换也需要进行大量的计算[MSDN]。总之,装箱拆箱是耗资源和时间的。而ArrayList集合在使用值类型数据时,其实就是在不断地做装箱和拆箱的工作,这显然是非常糟糕的事情。”

“啊,那从这点上来看,它还不如数组来得好了,因为数组事先就指定了数据类型,就不会有类型安全的问题,也不存在装箱和拆箱的事情了。看来他们各有利弊呀。”

“说得非常对,C#在2.0版之前的确也没什么好办法,但2.0出来后,就推出了新的技术来解决这个问题,那就是泛型。”

A.13 泛型

“泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以将类型参数用作它所存储的对象的类型的占位符;类型参数作为其字段的类型和其方法的参数类型出现[MSDN]。我读给你的是MSDN的原话,听起有些抽象,我们直接来看例子。首先泛型集合需要System.Collections.Generic的命名空间。而List类是ArrayList类的泛型等效类。该类使用大小可按需动态增加的数组实现IList泛型接口。其实用法上关键就是在IList和List后面加‘’,这个‘T’就是你需要指定的集合的数据或对象类型。”

“此时,如果你再写‘arrayAnimal.Add(123);’或者‘arrayAnimal.Add(“HelloWorld”);’结果将是?”

“哈,编译就报错,因为Add的参数必须是要Animal或者Animal的子类型才行。”

“至于‘叫声比赛’的写法就完全相同,这里就不写了。来,说说你对泛型集合List的感受。”

“我是这样想的,其实List和ArrayList在功能上是一样的,不同就在于,它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型安全问题和装箱拆箱的性能问题了。强,够强,怎么想到的,真是厉害。”

“是呀,这个改造的确是非常的好,不过显然C#语言的设计者也并不是一开始就明白这一点,也是通过实践和用户的反馈才在C#2.0版中改进过来的。巨人也有会走弯路的时候,何况我们常人。通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生并实现类型特定的成员。此外,如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型(并优于从非泛型基集合类型派生的类型),因为使用泛型时不必对元素进行装箱[MSDN]。”

“当然是泛型好呀,它可是集ArrayList集合和Array数组优点于一身的好东西,有了它,ArrayList就显得太老土了。”

“至于泛型的知识还有很多,这里就不细讲了,你自己去找资料研究吧。”

“好的好的,其实已经有些明白是怎么回事了。我自己去研究吧。”

A.14 委托与事件

“蔡老师,能不能给我讲讲委托与事件,我认真地看过书,也研究过,但实话说,我至今也不是很明白它的作用和好处。”

“哈,委托是对函数的封装,可以当作给方法的特征指定一个名称。而事件则是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程[PC#]。事件其实就是设计模式中观察者模式在.NET中的一种实现方式。要详细讲就有得讲了,不过在这可以举个例子,就当抛砖引玉吧。”

“好的,好的。”

“你先建一个控制台应用程序。我们的需求是,有一只猫叫Tom,有两只老鼠叫Jerry和Jack,Tom只要一叫‘喵,我是Tom’,两只老鼠就说‘老猫来了,快跑’。你来分析一下,这里应该有几个类,如何处理类之间的关系?”

“当然应该是有Cat和Mouse类,在Main函数中执行,当Cat的Shout方法触发时,Mouse就执行Run方法。不过这里如何让Shout触发时,通知两只老鼠呢?显然老猫不会认识老鼠,也不会主动通知它们‘我来了,你们快跑’。”

“你说得没错,的确是这样。所以在Cat类当中,是不应该关联Mouse类的。此时用委托事件的方式就是最好的处理办法了。注意,委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为[MSDN]。委托对象用关键字delegate来声明。而事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们[MSDN]。事件对象用event关键字声明。”

“这里就是声明了一个委托,显然委托名称叫做CatShoutEventHandler,而这个委托所能代表的方法是无参数、无返回值的方法。然后声明了一个对外公开的public事件CatShout,它的事件类型是委托CatShoutEventHandler。表明事件发生时,执行被委托的方法。关键的代码解释后,我们来看实际代码。”

“我问你,为什么CatShout()是无参数、无返回值的方法?”

“因为事件CatShout的类型是委托CatShoutEventHandler,而CatShoutEventHandler就是无参数、无返回值的。”

“说得对,就这么简单。我们再来看Mouse,它更加简单。”

“关键是Main函数的写法。”

“这里需要解释一下,new Cat.CatShoutEventHandler(mouse1.Run)的含义是实例化一个委托,而委托的实例其实就是Mouse的Run方法。而‘cat.CatShout +=’表示的就是‘cat.add_CatShout(new Cat.CatShoutEventHandler(mouse1.Run))的意思。’”

“哦,原来‘+=’就是增加委托实例对象的意思,以前我一直不明白这里的含义。那要是这么说,应该也有‘-=’,移除委托实例的操作了?”

“当然当然,所谓‘-=’不过就是‘remove_CatShout()’的含义了,使用了它,就等于减少一个需要触发事件时通知的对象。我们来看运行后的结果。”

喵,我是Tom。
老猫来了,Jerry快跑!
老猫来了,Jack快跑!
“我还有个问题,我看到我在写.NET应用程序或者Web程序时,总是看到IDE生成的事件参数。比如private void button1_Click(object sender,EventArgs e),这里的sender和e有什么用呀?”

“问得好,我们来改造一下这个例子,你就知道这两个参数是做什么用的了。”

“首先我们增加一个类CatShoutEventArgs,让它继承EventArgs,EventArgs是包含事件数据的类的基类[MSDN]。换句话说,这个类的作用就是用来在事件触发时传递数据用的。我现在写了它的一个子类CatShoutEventArgs,当中有属性Name表示的就是CatShout事件触发时,需要传递Cat对象的名字。”

“然后改写Cat类的代码,对委托CatShoutEventHandler进行重定义。增加两个参数,第一个参数object对象sender是指向发送通知的对象,而第二个参数CatShoutEventArgs的args,包含了所有通知接受者需要附件的信息。在这里显然就是老猫的名字信息。”

“哦,原来object sender就是传递发送通知的对象,而EventArgs是包含事件数据的类,怪不得。”

“Mouse类也发生了一点小变化。由于有了传递过来的猫的名字,所以显示的时候可以指示是老猫谁谁谁来了。”

“Main函数的代码没有变化,而结果显示不一样了。”

喵,我是Tom。
老猫Tom来了,Jerry快跑!
老猫Tom来了,Jack快跑!
“啊,原来委托事件这么简单。看来我以前是没认真学。那蔡老师,我们下面学什么?”

0%