C#读书笔记之继承、接口与抽象类续
接下来说明最开始提出的问题:为什么要使用接口和抽象类?
刚接触接口和抽象类的概念的朋友大部分都会和我有一样的困惑吧:这两玩意到底有什么用,而且还这么像,有什么区别?
继续用两个例子说明分别说明接口和抽象类是干什么用的。
还是我们的小商铺。我们知道,电器都有一个通电开关功能,但是,不同的电器通电之后产生的效果是不一样的,电冰箱是制冷,电灯是发亮,电视机是放节目。现在假设有三个类,
Fridge,Light,和Televison,他们都有两个方法PowerOn(),PowerOff():
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Blog1 { class Program { static void Main(string[] args) { } } class Television { //数量 public int Count { get; set; } public void PowerOn() { Console.Write("电视机开了,正在放映节目。"); } public void PowerOff() { Console.Write("电视机关闭,正在放映节目。"); } } class Fridge { //数量 public int Count { get; set; } public void PowerOn() { Console.Write("冰箱开了,里面好冷。"); } public void PowerOff() { Console.Write("冰箱关了,温度回升。"); } } class Light { //数量 public int Count { get; set; } public void PowerOn() { Console.Write("电灯开了,房间变亮了。"); } public void PowerOff() { Console.Write("电灯关了,房间又暗了。"); } } }
这个时候,当我们想要构建一个类似前面的Electronics类来实现继承时,我们会发现无从下手。虽然Fridge,Light,和Televison都有一个PowerOn和PowerOff方法,但它们内部的实现是不同的。该怎么办呢?这就是抽象类大显身手的地方了。我们定义一个抽象的Electronics类,修改后的部分代码如下:
abstract class Electronics { public int Count { get; set; } public abstract void PowerOn(); public abstract void PowerOff(); } class Light:Electronics { public override void PowerOn() { Console.Write("电灯开了,房间变亮了。"); } public override void PowerOff() { Console.Write("电灯关了,房间又暗了。"); } } class Fridge : Electronics { public override void PowerOn() { Console.Write("冰箱开了,里面好冷。"); } public override void PowerOff() { Console.Write("冰箱关了,温度回升。"); } } class Television { //数量 public int Count { get; set; } public void PowerOn() { Console.Write("电视机开了,正在放映节目。"); } public void PowerOff() { Console.Write("电视机关闭,正在放映节目。"); } }
来看看合前面继承的区别:
1.定义Electronics类时使用了abstract关键字,且类内PowerOn和PowerOff也有abstract关键字,并且没有指定要做什么的代码。
2.继承于Electronics的Fridge类和Light类都写了PowerOn,PowerOff方法具体的实现,且加了override关键字。
这是抽象类和普通类的区别之处,abstract关键字表面了这个类是一个抽象的类。何为抽象?其实“家电”本身就是一个抽象概念,假如有顾客说:请给我一个‘家电’。商铺自然没法给出‘家电’这么一个东西,因为家电是一个抽象概念,表示的是包括电视机,电冰箱,电灯,空调等等一类物品的统称。而抽象方法,可以理解为不指名具体实现的方法,它只能在抽象类中定义,定义的方式就如例子中的PowerOn何PowerOff,它只是指出了这个抽象类有一个功能,但是这个功能的具体作用,必须由继承了这个抽象类中的派生类实现。
现在来思考一下使用抽象类有什么用:看了上面的代码,可能有些人会问,既然抽象类的实现必须在其子类中重新写,那我又何必定义它呢,如果有20种电器,我还是得写20种PowerOn和PowerOff,根本没有减少我的代码量。在这个例子中是的,但是别忘了我们前面说过的继承的作用:虽然抽象类的具体实现必须得在子类中重写,但是我们起码拥有了一个统一的类的格式。我们知道了继承于这个抽象类的类都有而且必须有一个PowerOn和PowerOff方法,这是十分重要的。
假如现在商铺需要完成另外一个功能,它需要给顾客展示各种商品通电后的作用。我们可以写这么一个方法
class Shop { public void Show(Electronics ele) { ele.PowerOn(); ele.PowerOff(); } }
在这个方法中,我们传入了一个抽象类Electronics类型的对象ele,并且调用了它的PowerOn方法和PowerOff方法,利用前面的知识我们知道,我们同样可以传入Fridge和Light类型的对象,因为它们继承于Electronics,而且在这里会调用Fridge和Light类型对象内部已经实现的PowerOn和PowerOff方法。
当然,我们不能忘了抽象类和普通类的区别,比如说它同样有Count属性,因为Count不是抽象的,所以子类可以直接继承。
接下去说接口:
接口是一个比抽象类更抽象的概念。粗看之下它有点类似于只定义了抽象方法的一个抽象类,但是,本质上它们是有区别的。看下面的例子。
还是这家商铺,这家商铺里有电视机,收音机,电灯必需品,然后主人还养了一只鹦鹉,这只鹦鹉会唱歌。我们知道,电视机是能发光还能发出声音的,而收音机只能发出声音,电灯只能发光,这只会唱歌的鹦鹉也能发出声音。现在,这个商铺主人有这么两个需求:当天黑了之后,他需要一点亮光,这个时候他可以选择打开灯或者电视机,当他无聊的时候,他喜欢打开电视机听歌,或者打开收音机听歌,或者听这只鹦鹉唱歌。我们把这两个需求定义为NeedLight()和NeedSong(),如下:
class Master { public void NeedLight(** lighter) { //提供光 } public void NeedSong(** singer) { //唱歌 } }
在这里,我们需要提供一个“发光体”和一个“歌唱者”的实例进去,调用它们的方法。思考一下,我们该传个什么类型进去?
在正式介绍接口前,我们先提供几个可行方案,并一一指出它们的缺陷。
首先是最简单又最麻烦的一种。
class Televison { public void Shine() { Console.WriteLine("电视机发光"); } public void Sing() { Console.WriteLine("播放唱歌节目"); } } class Radio { public void Sing() { Console.WriteLine("收听唱歌电台"); } } class Light { public void Shine() { Console.WriteLine("电灯亮了"); } } class Parrot { public void Sing() { Console.WriteLine("鹦鹉在唱歌"); } }
我们给四个类都定义了各自的唱歌(sing)和发光(Shine)方法,在Master类里并可以这么调用:
class Master { public void NeedTelLight(Televison lighter) { lighter.Shine(); } public void NeedLitLight(Light lighter) { lighter.Shine(); } public void NeedTelSong(Televison singer) { singer.Sing(); } public void NeedRadSong(Radio singer) { singer.Sing(); } public void NeedParSong(Parrot singer) { singer.Sing(); } }
学了继承之后,我们自然不会满足于这种方法,它的缺点太明显了,代码重复,可扩展性也差。所以,我们可以尝试第二种方法,利用一个抽象类:
abstract class SingerAndLighter { public abstract void Shine(); public abstract void Sing(); } class Master { public void NeedLight(SingerAndLighter lighter) { lighter.Shine(); } public void NeedSong(SingerAndLighter singer) { singer.Sing(); } }
让电视机,收音机,电灯和鹦鹉继承了这个抽象类后,我们并可以传入一个SingerAndLighter类型的参数到NeedLight和NeedSong方法里。但是等等,这种设定未免有些荒谬,因为这就意味着我们有了一个会唱歌的电灯和一只会发光的鹦鹉。这明显是不符合实际情况的。
那么,下面这种方法又如何呢。
abstract class Singer { public abstract void Sing(); } abstract class Lighter { public abstract void Shine(); } class Master { public void NeedLight(Lighter lighter) { lighter.Shine(); } public void NeedSong(Singer singer) { singer.Sing(); } }
我们定义了一个能唱歌的类Singer和一个能发光的类Lighter,并且让NeedLight方法接受一个Linghter类型的对象,NeedSong方法接受一个Singer类型的对象。这样它们各司其责,似乎可以解决问题了。可是,别忘了,我们还有一个特殊的存在:电视机,它能同时实现发光和唱歌的功能。这是个麻烦,因为类是没法同时继承多个基类的。除非我们继续引用前面定义好的那个SingerAndLighter类,让电视机继承这个类,但这又意味着我们需要在Master类里添加两个新方法来处理这个SingerAndLighter类型的参数。
其实我们离答案已经很近了。在一开始,我们在处理的方向上出了一点问题。再来看看我们需要定义的四个类:电视机,收音机,电灯和一只会唱歌的鹦鹉。我们一直在寻找一种方法,能够将它们归类成一个抽象概念,就像电视机,电冰箱之于家电,但这是错误的,因为我们需要的不是它们本身的共同点,而是它们能做的事情的,即方法的共同点。
这个共同点我们把它称为接口。
何为接口?以一个通俗的概念来说,接口表示的就是一个“可以做的事情”的集合。电视机能发光,能唱歌,这就是它“可以做的事情”,在这里,我们需要做的就是把它们可以做的事情提取出来,形成接口。看如下代码。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Blog1 { class Program { static void Main(string[] args) { Televison tel = new Televison(); Radio rad = new Radio(); Light lig = new Light(); Parrot par = new Parrot(); Master LinJ = new Master(); LinJ.NeedLight(tel); LinJ.NeedLight(lig); LinJ.NeedSong(tel); LinJ.NeedSong(rad); LinJ.NeedSong(par); Console.ReadLine(); } } class Televison:ISinger,ILighter { public void Shine() { Console.WriteLine("电视机发光"); } public void Sing() { Console.WriteLine("播放唱歌节目"); } } class Radio : ISinger { public void Sing() { Console.WriteLine("收听唱歌电台"); } } class Light : ILighter { public void Shine() { Console.WriteLine("电灯亮了"); } } class Parrot : ISinger { public void Sing() { Console.WriteLine("鹦鹉在唱歌"); } } interface ISinger { void Sing(); } interface ILighter { void Shine(); } class Master { public void NeedLight(ILighter lighter) { lighter.Shine(); } public void NeedSong(ISinger singer) { singer.Sing(); } } }
在这里,我们先定义了两个接口ISinger和ILigter,分别实现了Sing方法和Shine方法,然后让电视机,收音机,电灯和鹦鹉继承了相应的接口,并且实现了具体的方法。和用抽象类相比,这里的电视机同时继承了两个接口,这是接口允许的。
通过上面的例子可以很清楚地说明了为什么要使用继承,抽象类,接口,接下去我要引用一段网上看到的话来说明它们之间的关系。
铁门木门都是门(抽象类),你想要个门我给不了(不能实例化),但我可以给你个具体的铁门或木门(多态);而且只能是门,你不能说它是窗(单继承);一个门可以有锁(接口)也可以有门铃(多实现)。门(抽象类)定义了你是什么,接口(锁)规定了你能做什么(一个接口最好只能做一件事,你不能要求锁也能发出声音吧(接口污染))。
这段话里的很多概念这里没讲到,不过不难理解。其中“门(抽象类)定义了你是什么,接口(锁)规定了你能做什么”这句话阐述了接口和抽象类最本质的区别。抽象类实现了 “is-a”关系,“电冰箱 is a 家电”。接口实现了“can do”关系,“鹦鹉 can 唱歌”。这便是它们最本质的区别。尽管有时候我们觉得使用抽象类和使用接口可以完成同样的功能,但是仔细想想我们要定义的类之间的关系,便能明白究竟哪一种才是合适的选择。
优质内容筛选与推荐>>