新手上路,可能有很多错误,欢迎指点。
1、 什么是接口(interface)。
接口定义了一组公共的规范或者能力,实现该接口的类便具有这个规范能力。一个简单的例子,假设有一个接口叫飞翔(IFlyable),该接口定义了一个叫飞(Fly())的方法,所有实现了该接口的类(如鸟类(Bird)或者飞机(Plane))便会具有飞翔的能力。
2、 接口和抽象类(Abstract class)。
特别地,接口并不等同于抽象类,抽象类是指具有抽象成员的类(class),一个类只要有一个(当然也可以有多个或者全是)抽象成员,该类就是(而且必须是)抽象类。并且在抽象类中被abstract标记的成员不能再被标记为private或者virtual。抽象类不能被实例化,派生自抽象类的类要想自身不再成为抽象类,则必须要重写(override)其父类的所有抽象成员。除了这些之外,抽象类和普通类没有什么区别。从宏观上讲,抽象类的作用就定义了一组公共的特性或操作。下面说说它们之间的异同。
A.抽象类和接口的相同之处:1)都是定义了某些公共的内容;2)都不能被实例化。3)接口的方法和抽象类的抽象方法都不能有方法体的实现(就连{}也不能有),并且接口和抽象类的抽象成员都只能包含方法一类的成员(属性、方法、委托、事件、索引器等)二不能包含变量一类的成员(字段)。
B.抽象类和接口的不同之处:1)抽象类本质上是类,因此在继承是应该遵循类的继承(单一继承),二接口可以多重继承(实现);2)派生自某一接口的类必须实现该接口的所有成员,派生自某一抽象类的类不必实现其父类所有抽象成员(如果这么做的话,该类依然为抽象类)。3)抽象类中可以有非抽象成员,甚至可以全为非抽象成员,接口中的所有成员必须是没有实现体的。4)抽象类中的抽象成员的可访问性必须为public或者protected,接口的成员不能有访问修饰符,并且被默认为public。5)从宏观上讲,派生自某类(包含抽象类)的多个子类之间一般具有紧密的联系。例如假设秃鹰和大雁都继承自鸟类,它们之间是具有紧密联系的。而实现同一接口的类之间的联系是很松散的,例如鸟类和飞机都可以实现飞翔这个接口,甚至将来还有某种神秘的事物亦可以派生自该接口,但是它们之间的联系就不是那么紧密。
3、 实现接口。
一个类派生自某个接口就称该类实现了这个接口。一个类如果实现了某个接口,则必须实现该接口的所有成员。实现接口有两种方式:隐式实现和显式实现。下面以代码为例详细说明之。
//定义接口飞翔
public interface IFlyable
{
void Fly();//定义方法飞
bool HasFeather{get;} //定义属性是否有羽毛
}
//A.隐式实现接口
class Bird:IFlyable //定义类Bird派生自IFlyable
{
public void Fly()
{
Console.WriteLine("I can fly!");
}
public bool HasFeather
{
get
{
return true;
}
}
}
B.显式实现接口
class Plane : IFlyable //定义类Plane派生自IFlyable
{
void IFlyable.Fly()
{
Console.WriteLine("I canfly but I have no Feather!");
}
bool IFlyable.HasFeather
{
get { return false; }
}
}
从以上可以看出实现接口和显式实现接口不同之处仅在于后者在实现时成员名前多了接口名的限制,格式为 接口名.成员名。两者有什么区别呢?
当我们在一个类中隐式实现一个接口的方法时,确切地说并不是在为接口的方法做实现代码,而是新引入了一个方法,但是这个方法特殊在刚好和它的接口中的方法具有完全相同的签名和返回值类型,于是CLR便假设这个方法是对接口方法的一个实现。若该方法被标记为virtual ,CLR仍然会如此假设。这样的话在该类的元数据方法列表中就会有两个方法(一个为接口定义,一个是类型新引入的方法)指向同一个对它的实现。这样的话就不能在该类中定义一个和接口中方法签名相同但是又和它完全不相关的方法。虽然可以使用new来隐藏接口中的成员,但是如果将它隐藏了的话,岂不是又不能为这个方法做实现了?这显然违反“如果实现了某个接口则必须实现该接口的所有成员”这一原则。
相比之下,显式实现接口就可以解决这个问题。上边的void IFlyable.Fly()就明确告诉CLR我这个类中的Flay()方法是对接口IFlyable中Fly()方法的实现,这样即使在该类中再引入一个和它同签名的方法,
public void Fly()
{
……
}
CLR也不会假设它是对接口的实现,而是只把它当做新引入的方法,上面的问题就轻松解决了。如果还在上面的代码Plane类中加入一个方法如下:
public void Fly()
{
Console.WriteLine("我能飞得更高!");
}
Program中代码如下:
class Program
{
static void Main(string[]args)
{
Plane p = new Plane();
p.Fly();
}
}
运行结果如下
说明程序执行了新引入的方法public voidFly()而不是对接口的实现方法void IFlyable.Fly()。
在刚才的例子中显式实现接口的成员前是没有访问修饰符限定的,被默认为私有的,这是必须的,为了防止它在该类的实例中被访问到,像上面的例子p.Fly(),就没办法区分访问的是哪个方法(因为这样的方法有两个)。既然是私有的当然也不能被标记为virtual来在子类中重写。要想在类实例中访问到该接口的方法,则必须把该类转换成接口类型,这样就只剩下接口中的成员,不再会引起混淆。代码如下:
只需要将原来Main方法中的
Plane p = new Plane();
改为
IFlyable p = new Plane();
运行结果如下:
4、 接口、抽象方法、虚方法。
本来接口和虚方法是没有多大联系的,因为有些易混淆的概念,在这里只是顺便提下。
前边已经说过接口和抽象方法的概念,因此这里直接拿来比较。虚方法(确切的说应该是类的虚拟成员,包含属性、方法、委托、事件、索引器等,由于都是以方法的形式出现的,所以先简称为虚方法)是在方法前加virtual关键字修饰,表示在它的子类中可以被覆盖(override),同接口和抽象方法一样是用来实现多态(Polymorphism)的。虚拟方法和抽象方法在其子类中被覆盖后依旧是虚拟的,所以在其子类的子类中同样可以被覆盖。和抽象方法不同的是,如果子类不覆盖虚拟方法,则该方法将保持父类中的实现,并且不影响子类的实例化,而在接口和抽象类中强制都要求实现它们的类必须为其抽象方法或接口成员做自己的实现,特别地,借口成员被实现后默认不再是虚拟的,举个简单的例子:接口A中有一个方法a(),类B实现了接口A,类C继承自类B,那么默认情况下在C中将无法覆盖方法a(),除非在B中隐式实现方法a()时已将其标记为虚拟的。另一个不同于抽象方法的地方是虚拟方法必须要有实现部分,哪怕一句代码也没有也必须保留方法的{}。而抽象方法或者接口成员必须不能有实现部分,包括{}。另外虚拟方法和抽象方法都不能是私有的,接口成员虽然默认是公共的(但不能手动标记任何访问属性)但是也不能被虚拟化,因为它本身已经是抽象的了,就像抽象方法不能同时被标记为虚拟方法一样。
5、 关于父类和子类实现同一接口的问题
这个很容易让人感觉迷惑。既然父类已经实现了某个接口,那么按理说子类就没必要再去实现这个借口了吧,因为子类会继承被父类实现的接口成员。那为什么还有那么多父子类都实现同一接口的例子呢?例如类WebRequest和其子类 HttpWebRequest都实现了ISerializable接口。
还是拿一开始举得那个例子来说明吧。鸟类实现了飞翔这个接口,所以鸟类具有飞的能力,大雁和秃鹰都继承自鸟类,如果它们不实现飞翔这个接口也没什么不可以,以为它们的父类鸟类实现了这个接口,因此它们都能飞。但假如鸟类在实现飞这个方法时没有将其虚拟化(这也是一般的做法),其子类大雁和秃鹰将只能使用同鸟类一样的飞法,可现实中这是不合理的,大雁应该有大雁的飞翔方式,秃鹰也应该有自己独有的飞行方式才对。所以要想有这样的效果,大雁和秃鹰必须也实现飞翔这个接口,来实现各自的多态。这样做就不会依赖其父类必须虚拟化飞这个方法,减少了类和类之间的耦合性。代码如下:
public interface IFlyable
{
stringFly();
}
class Bird:IFlyable
{
public string Fly()
{
return"I canfly!";
}
}
class DaYan : Bird, IFlyable
{
public new string Fly()//注意
{
return"我飞得远!";
}
}
class TuYing:Bird,IFlyable
{
string IFlyable.Fly()//注意
{
return"我飞得高!";
}
}
class Program
{
static void Main(string[]args)
{
IFlyablef1 = new DaYan();
IFlyablef2 = new TuYing();
Console.WriteLine("我是大雁:{0}\n我是秃鹰:{1}", f1.Fly(), f2.Fly());
}
}
运行结果如下:
上面有个需要注意的地方已经在注释中标出,就是在父类和子类同时实现一个接口时,子类必须显式实现该接口或者使用new将父类的相同成员隐藏。另外,子类没有必要为接口的全部成员做实现代码,只需要实现和父类不同的那部分接口成员,其它的接口成员将从父类继承。
6、 总结
实现多态的关键字主要有interface、abstract、virtual、override。其中interface声明接口,abstract和override成对出现在父子类中表示实现抽象类的抽象成员,virtual和verride成对出现在父子类表示重写父类的虚拟成员。
接口的出现有利于减少对象之间的耦合性,使对象之间的关系变的松散,符合面向对象思想,并且允许多重继承,使得编程语言更加灵活。
有人说要尽量使用接口继承而避免使用类继承,原因是类继承容易引起类爆炸(因为操作系统能在同一时段够加载的模块是有限的),这个不是没有道理。但是接口继承和类继承有各自不可取代的优势,因此我觉得避免使用类继承一说有点不合适。
接口在设计时要尽量做到功能单一,否则会出现接口污染,一个类为了实现接口中的某一方法而不得不去实现其它一些可能和该类无关的方法。
最后补充一点,面向接口的编程中将接口作为一个公共的标准,实现某个功能的对象需实现一个事先定义好的接口,该接口是被公开的(对象的调用者和被调用者均知道该接口的存在),需要调用它的对象将这个类先转化为该接口,然后再调用接口中的已声明的某个方法便可以完成相应功能。
参考资料http://www.cnblogs.com/pavel/archive/2011/05/09/interface-implement.html