浅谈接口与抽象类的区别
从代码的语法定义和使用逻辑两个方面浅谈接口与抽象类的区别.
1 语法定义篇
(1)首先是定义语法
接口
接口的定义是
[访问修饰符] interface 接口名
{
// 接口成员
}
抽象类
抽象类的定义是
[访问修饰符] abstract class 类名
{
// 类成员
}
定义语法中接口关键字interface,类关键字class没什么差异,抽象类多一个abstract修饰
(2) 成员类型
接口
对于接口,它是定义的一类能力,因此以功能为主,面向一类抽象能力,所以成员只与方法有关. 那么记忆接口能定义什么,就记住方法便可.
举个例子,对于接口成员可以包含:方法、属性、事件和索引器.
实际上可以知道,属性是特殊的方法,事件是一个私有的委托变量加上两个方法,而索引实际上就是属性,因此对于接口成员的记忆,可以归结为“方法”
抽象类
抽象类的成员与一般类成员没什么区别,只不过抽象成员必须在抽象类中. 而在抽象类中一般定义的抽象成员有方法、属性、事件和索引器.
另外,抽象类可以有构造方法,其作用是为非抽象成员进行初始化,而不是作为创建抽象类的实例而使用
注:即使抽象构造方法不能被外界调用,但是也不能设定为private,因为其派生类会默认调用无参构造方法
(3)成员访问修饰
接口
接口就是为了被实现的,即每个成员都是要被别的类进行实现的,那么每个成员都应该是public类型,因此不用设定接口中成员类型的访问修饰符,默认为public,一般语法为
返回类型 方法名(参数列表);
抽象类
抽象成员必须设定为public,其余没有要求
(4)各抽象成员的定义
接口
方法的定义:
返回类型 方法名(参数列表);
属性的定义:
返回类型 属性名{ get; set; }
事件的定义:
event 委托名 事件名;
索引的定义:
返回类型 this[索引类型] { get; set; }
例如:
public interface IMyInterface { void Func(); string Value { get; set; } event EventHandler MyEventHandler; string this[int index] { get; set; } }
注:索引器与属性的get与set不同于自动属性,是可以选择只读、只写、还是读写的 一般接口都已I命名开始
抽象类
其抽象成员定义类似,但是一般在抽象类中的抽象成员以方法和属性居多.
首先抽象成员可以来自接口获得,只需要在每一个成员的前面加上public abstract即可
例如:
public abstract class MyAbs : IMyInterface { public abstract void Func(); public abstract string Value { get; set; } public abstract event EventHandler MyEventHandler; public abstract string this[int index] { get; set; } }
其次,抽象类可以来自与某个基类,将基类的虚方法用abstract进行修饰,例如
public class BaseClass { public virtual void Func() { // 方法体 } } public abstract class MyAbs : BaseClass { public abstract void Func(); }
注:
1、方法没有方法体是指参数括号写完后没有花括号,直接分号结束;如果写成
public abstract void Func() { }
不叫作无方法体,只能称作空实现,这样会出现语法错误.
2、抽象成员必须是public的,同时由abstract修饰.
3、接口中的事件定义与抽象类中的事件定义意义和方法定义相同:
-> 在接口中定义一个事件,相当于同时定义了add和remove没有方法体的方法
void add_MyEventHandler(EventHandler value);
void remove_EventHandler(EventHandler value);
-> 在抽象类中,相当于两个abstract方法
public abstact void add_MyEventHandler(EventHandler value);
public abstact void remove_EventHandler(EventHandler value);
3、在其子类中可以被实现,对于接口实现add和remove方法,对于抽象类重写它们
(5)抽象成员的实现
实现接口
实现接口的类分三种,一是抽象类,二是一般的类,再就是结构. 而实现方式又分为实现接口和显式实现接口.
抽象类
抽象类实现接口,只需要将接口中的成员与方法添加好就行了,但是必须保证成员的访问级别为public
例如:
public abstract class MyAbs : IMyInterface { public void Func() { // 实现代码 } public string Value { get; set; } public event EventHandler MyEventHandler; public string this[int index] { get { // 实现代码 } set { // 实现代码 } } }
注意:
1、此处一定不允许丢掉public,因为接口就是用来定义方法,最后是要被访问的,如果不设为public,那么会出现问题.
2、此处的属性与接口中的属性意义是不同的,此处为自动属性,在代码的后台编译器会自动生成一个字段,并填补get和set读取器.
3、此处的事件也与接口中的事件意义不同(从代码角度看几乎一样),这里的事件,编译器会自动生成一个同名的私有委托,并将add方法与remove方法补全.
如果不希望实现接口中的方法,可以将接口成员直接copy下来,在前面加上abstract关键字即可. 例如
public abstract class MyAbs : IMyInterface { public abstract void Func(); public abstract string Value { get; set; } public abstract event EventHandler MyEventHandler; public abstract string this[int index] { get; set; } }
那么此时的各个成员,其本质与接口中的成员一样,只有方法签名,没有实现体.
对于“显式实现接口”,其实现方式就是将成员写成
接口名.成员名
的形式. 显式实现接口与一般的实现区别在于显式实现的成员只有接口对象才能调用.
实现方式代码如下
public abstract class MyAbs : IMyInterface { void IMyInterface.Func() { // 方法体 } string IMyInterface.Value { get; set; } event EventHandler IMyInterface.MyEventHandler { add { // 实现代码 } remove { // 实现代码 } } string IMyInterface.this[int index] { get { // 实现代码 } set
{ // 实现代码 } } }
注意:
1、这里显示实现的接口,不允许有访问修饰符. 因为显式实现的接口只能由接口对象来调用,这里无论写什么都会出现问题.
2、这里事件不允许使用默认系统自定义的方式,需要自己添加add方法和remove方法
3、另外显示实现接口的成员不允许使用abstract进行修饰.
普通类实现接口
对于普通的类实现接口,只需要将接口的成员的代码实现补全即可,这里与抽象类是一样的,同样对于事件,可以手动添加add方法和remove方法.
如果在实现的方法中,有部分需要在子类中被重写,那么只需在方法前加上virtual进行修饰即可,示例代码如下:
public class MyClass : IMyInterface { public virtual void Func() { // 方法体 } }
注:
1、这里的成员访问修饰符也必须是public.
2、这里同样可以“显式实现接口”,规则与方法与抽象类中介绍的一致
-> 不允许使用virtual修饰成员
-> 事件必须使用“事件访问器”语法
-> 成员没有访问修饰符
实现接口的除了类以外,接口也可以实现接口
实现抽象类
抽象类的实现与接口实现由点类似,不过这里不存在显式与非显式罢了. 在实现抽象类的过程中,抽象类的每一个抽象成员都需要提供实现代码,因为抽象类中包含没抽象成员,因此继承自抽象类的类,只用提供重写代码即可.
不过在语法中与接口的实现还是有点不一样. 接口中成员的实现与一般的写法是一样的,但是实现抽象类的成员,每一个成员前面都要有override的修饰. 代码如下:
public class MyClass : MyAbs { public override void Func() { // 实现代码 } public override string Value { get; set; } public override event EventHandler MyEventHandler; public override string this[int index] { set { // 实现代码 } get { // 实现代码 } } }
注:
1、这里实现的成员必须使用public override进行修饰.
2、事件可以不自己提供add方法与remove方法
-> 此时编译器会自动添加一个私有的委托变量,以及add方法与remove方法
3、这里必须保证每一个成员都要重新实现,这里示例代码中的属性是自定属性,因此看起来与抽象类中的一样,但是编译器会自己添加私有字段与get方法和get方法.
4、如果依旧不需要某些方法提供方法体,即仍旧是抽象成员,那么需要补全其他方法,将不需要方法提的方法用abstract修饰即可,同时不要忘记类也应该使用abstract进行修饰,因为抽象成员只能允许在抽象类中存在.
属性的实现
属性定义为抽象类型可以由三种形式:只读属性、只写属性和读写属性.
在实现属性的时候,前面提到的例子都是读写属性,下面来看看另外两种.
只读属性
只读属性的定义为:
public interface IMyInterface { string Value { get; } } public abstract class MyAbs { public abstract string Value { get; } }
注:通过前面的介绍,可以知道实现抽象成员,接口与抽象类是类似的,那么此处介绍以接口的实现为规范描述.
实现该属性的有两种方案,第一种就是提供一个字段,并添加读取器的实现代码,例如
public class MyClass : IMyInterface { private string _value; public string Value { get { return this._value; } } }
第二种方案便是补全两种读取器,例如
public class MyClass : IMyInterface { private string _value; public string Value { get { return this._value; } set { this._value = value; } } }
注:
1、对于第二种方案,在显式实现接口的时候不能使用,因为在接口中不存在补全的读取器.
2、对于添加进来的读取器可以使用访问修饰符进行修饰,例如protected等.
(6)使用逻辑篇
接口与抽象类都是抽象的数据类型,虽然使用中有很多比较模糊的地方发,也就是说部分情况接口与抽象类的使用时可以互换的. 但是详细来说接口与抽象类还是有很多不同的地方. 总的来说就是注重功能的时候使用接口,注重对象的时候使用抽象类.
对于接口,从定义就可以发现是对方法的抽象,也可描述成对能力的抽象;而抽象类是对以事物的抽象,与具体的对象有关. 这么说很混乱,直接上例子看看.
先考虑以下接口代码:
public interface IMobilePhone { void Call(); void Message(); } public interface IProgram { void RunApp(); }
先对这两个接口做一个说明. 为了描述抽象类与接口在使用中的关系,我打算描述以下手机在生活中的情况,这样区分接口与抽象类的关系表现会明显许多.
这里定义一个接口IMobilePhone,用来表示手机最基本的能力“打电话”和“处理短信”,这里不考虑较复杂的情况,比如接电话与拨电话,发短信与收短信等.
定义接口IProgram用来描述智能机能够运行应用程序. 同样不考虑较复杂情况.
我们知道,手机是一个很泛的概念,比如你试想一下——你见过手机吗?或许说你只能分辨什么是手机,如果让你来描述你明显的会用一个大家熟悉的另一部手机来比拟,比如“和苹果一样可以怎么怎么”,“和诺基亚5800一样可以怎么样怎么样”等. 因此说手机实际上是一个抽象的概念,可以将手机定义为一个抽象类,如下代码.
public abstract class MobilePhone : IMobilePhone { public virtual void Call() { Console.WriteLine(“打电话了”); } public virtual void Message() { Console.WriteLine(“处理电信了”); } }
注:
1、此处使用虚方法的原因是考虑大多数手机的电话处理与短信处理相似,只有一些比较高级的手机处理短信会有彩信、蓝牙信息、多媒体信息等,因此等到其他具体智能机时再考虑重写.
2、这里考虑手机的基本功能,为描述接口与抽象类的一些区别,因此省略其他无关信息.
那么此处手机是有了,但是什么型号?什么品牌?很明显手机只是一个概念,具体到手机还有品牌,型号等很多信息,那么可以用这个抽象类派生一个诺基亚手机出来. 不过像诺基亚这样的大厂商有5、6k的手机,也有充100元话费赠送的手机,很显然,诺基亚手机也是抽象类. 代码如下:
public abstract class Nokia:MobilePhone { public readonly string Mark = “Nokia”; public abstract string FaceDesigner { get; } public abstract string Keyboard { get; } }
注:
1、这里定义的两个只读属性,FaceDesigner表示板式,是翻盖还是直板;Keyboard表示键盘是虚拟还是非虚拟.
2、定义一个只读的字段,表示手机品牌,然后具体的手机由该类来派生.
下面就100元手机和Nokia Lumia 900作个模拟(赶个潮流!哈哈!!!)
100元机型
直接上代码:
public class Nokia1280 : Nokia { static int index; string numMark; string faceDesigner; string keyboard; public Nokia1280() { numMark = base.Mark + “ 1280 No” + (++index).ToString(“0000”); faceDesigner = “直板机”; keyboard = “物理键盘”; } public string NumMark { get { return this.numMark; } } public override string FaceDesigner { get { return this.faceDesigner; } } public override string Keyboard { get { return this.keyboard; } } }
注:
1、因为每台手机都有一个唯一的序列号,这里使用自增静态的index来描述该机型的产量,每生产一部手机,就会自动增加一次. 并将数据与品牌进行字符串处理,赋值给numMark,即可知道这款手机的型号与编号了.
2、添加构造方法,设定为直板机型与物理键盘.
3、实现每一个属性.
Nokia Lumia 900
由于这里是绝对的智能机,毫无疑问需要考虑可以执行应用程序的情况,因此该类继承自Nokia同时实现IProgram接口,但是智能机还有5800、N8等别的型号啊,所以很明显,智能机需要有一个类来进行描述,那便是一个智能机的抽象类. 代码如下
public abstract class NokiaApp : Nokia, IProgram { public abstract void RunApp(); }
然后再派生出Lumia900
public class Nokia_Lumia_900 : NokiaApp { static int index; string numMark; string faceDesigner; string keyboard; public Nokia_Lumia_900() { numMark = base.Mark + “ Lumia 900 No” + (++index).ToString(“0000”); faceDesigner = “直板机”; keyboard = “虚拟键盘”; } public override void Call() { Console.WriteLine(“用蓝牙打电话了”); } public override void Message() { Console.WriteLine(“处理短信、彩信、多媒体信息”); } public override void RunApp() { Console.WriteLine(“运行Windows Phone程序了”); } public string NumMark { get { return this.numMark; } } public override string FaceDesigner { get { return this.faceDesigner; } } public override string Keyboard { get { return this.keyboard; } } }
注:
1、智能手机实现IProgram接口,因此抽象类NokiaApp继承自Nokia,实现IProgram.
2、厂商可能会生产许多其他机型,因此在创建手机的时候需要考虑一个智能机的抽象类.
3、重写Call方法是由于智能机可以使用更多的扩展实现打电话的功能,比如蓝牙、可视等.
4、重写Message同样因为智能机可以处理多媒体,并且可以轻松地与网络进行交互.
那么中规中矩,以上是一个比较完整的实现接口与抽象类的实例,但在实际开发中不见得会用到这么详细与复杂,可能根本不需要接口,或者根本不需要中间的这么多抽象类,这里把这部分描述得如此详细主要是为了方便比较两种方法在实际使用中的差异.
开始比较
面向手机的基本应用
如果简单的描述电话等能力,似乎使用抽象类与接口没有多大区别,前面也讲过. 但是如果我手上的项目是一个考虑手机处理的一个项目,需要考虑手机的基本信息,包括电话功能、信息功能和一些简单的附属功能、比如闹钟等.
很明显,前文中的代码中使用Nokia这里抽象类足够描述所有信息了. 加上多态的实现,就可以很好的描述所有手机的基本信息.
在Main方法中添加如下代码,运行可以查看结果.
1 Random r = new Random(); 2 3 Nokia[] nks = new Nokia[100]; 4 5 for(int i = 0; i < nks.Length; i++) 6 7 { 8 9 if(r.Next(2) == 1) 10 11 { 12 13 nks[i] = new Nokia1280(); 14 15 } 16 17 else 18 19 { 20 21 nks[i] = new Nokia_Lumia_900(); 22 23 } 24 25 } 26 27 foreach(Nokia n in nks) 28 29 { 30 31 n.Call(); 32 33 n.Message(); 34 35 Console.WriteLine(“手机品牌是{0},外形设计是{1},键盘是{2}”, 36 37 n.Mark, 38 39 n.FaceDesigner, 40 41 n.Keyboard 42 43 ); 44 45 Console.WriteLine(); 46 47 }
注:这里只关注手机的全部基本功能,是面向一个手机对象的,换句话说需要包括所有的手机,但针对最基本和最全面的手机功能来进行实现,不用考虑较为复杂的手机扩展,很显然此处不太适合使用接口.
面向智能手机的应用
很明显,与上面的例子类似,我们可以从NokiaApp这个抽象类中派生出其他型号的智能手机,例如N9、5800、C7、X7、E7等.
要处理智能手机的信息,就可以使用NokiaApp这个抽象类与多态的实现,来处理所有的智能手机的打电话、信息处理、软件处理等所有功能. 很显然代码示例与上一个示例较类似,这里就不多列举了.
最后要提到的就是,这里面向的仍然是手机这个对象,因此也不太方便使用接口,应该使用抽象类.
面向电话与信息平台
上面都使用了抽象类型,但是面对的成员是手机这个看似具体的对象. 但是代码中仍然有比较专注于功能的地方.
比如我要实现一个短信处理平台,只需要最基本的短信收发即可. 例如登陆网络银行,会受到短信、刷卡消费会受到短信、每日财经消息也会被发送到手机上等等.
那么试想一下,此处与手机的什么最相关?很显然,只有Message方法,因此这里使用接口会比较方便. 添加如下代码,运行即可.
Random r = new Random(); IMobilePhone[] nks = new IMobilePhone[100]; for(int i = 0; i < nks.Length; i++) { if(r.Next(2) == 1) { nks[i] = new Nokia1280(); } else { nks[i] = new Nokia_Lumia_900(); } } foreach(IMobilePhone n in nks) { n.Message(); Console.WriteLine(); }
注:这里只针对一个Message方法,与什么手机没有任何关系,100元的一般手机可以实现,好几千元的智能手机也能实现,因此不用考虑那么复杂,使用接口变得更加容易. 因为此处面向的是功能,是一种能力.
面向运行软件能力的应用
再来看一个关于智能机应用的例子.
比如我现在要做一个网络交友平台,这个显然是针对智能机设定的一个功能,假定所有手机都已经安装完成客户端,我现在的应用只针对用户发来的数据包,即只负责与用户进行数据的交互. 那么完全没有必要针对所有智能手机这个对象,只需要使用RunApp方法即可,那么此处使用接口是再好不过的了.
综上讨论,可以得出结论了,对于使用逻辑上,接口与抽象类到底用哪一个,在于你所写程序的关注点,你的项目中更关注与功能与方法,那么使用接口比较好;若果是关注一类对象,那就使用抽象类了. 所以说接口是对一类功能的抽象,抽象类是对一类对象的抽象.
同时不要忘记,对于结构的继承关系,只能使用接口.
当然再怎么用,也不会像这样来写代码的,实在是太复杂了.