学习过C++的童鞋都应该知道,在C++中允许将一个类的成员函数定义为virtual(虚拟函数),定义为虚拟的函数,例如:
class CTest
{
public:
virtual void TestA();
virtual void TestB() = 0;
};
void CTest::TestA()
{
cout<<"Hello"<<endl;
}
class CTestChild : public CTest
{
public:
virtual void TestB();
};
void CTestChild::TestB()
{
cout<<"Hello World"<<endl;
}
int main(int argc, char* argv[])
{
CTest* pTest = new CTestChild();
pTest->TestA();
pTest->TestB();
}
形如第4行的函数声明称为“虚拟函数”,可以被子类的同名函数(返回值类型,参数也必须完全相同)覆盖;形如第5行的函数声明称为“纯虚拟函数”,这个函数没有函数体,必须被子类的同名函数(返回值类型,参数也必须完全相同)覆盖。
C#中也具有虚拟方法(使用virtual修饰的方法)和抽象方法(使用abstract修饰的方法),相当于C++中的虚拟函数和纯虚拟函数。
可以在C++中设计这样的一种类,这个类中的所有成员函数都为纯虚拟函数,即都没有方法体。这种类型的类非常有用。在C#中,语言设计者为它起了一个新的名字——接口。
接口在现实生活中非常常见,例如我们连接DVD影碟机和电视机,只要我们有这样的两个设备,使用电缆将它们连接在一起就可以了,无须考虑这两个设备是否可以相互兼容。这其中的道理非常简单,影碟机可以输出AV信号,电视机可以接受AV信号,所以可以连接。
所以接口并不是一个实际存在的插口,而是双方都共同遵守的一种行为准则。因为双方都遵守这样的规范,所以双方可以毫无障碍的联系在一起。
接口是面向对象中的最高抽象,接口只定义了要执行的方法,而没有”类属“信息,比如有一个Runnable接口,其中包含了一个run方法,那看到这个接口的人会知道这是一个”会跑的东西“,但具体是什么就不得而知了。但如果有一个Horse类来实现这个接口,那就很明确它的类型是”马“。
我们来看一个例子:
public abstract class Runnable {
public abstract void Run();
}
public class Mechanical{
}
public class Car : Mechanical{
}
public class Animal {
}
public class Cat : Animal {
}
代码中,我试图确立一种行为规范称之为“会跑的”(Runnable),有两个类需要遵循此规范,Car类和Cat类,显然汽车和猫都是会跑的东西,都可以实现抽象方法Run。
但问题出现了,Car类显然需要从机械类(Mechanical)继承,因为汽车是机械的一种,但显然并不是所有的机械都会跑(例如电动刮胡刀);而Cat类显然是从动物类(Animal)继承,但也不是所有的动物都会跑(例如鱼)。Car和Cat完全找不到相同的超类,但它们又可以遵循同样的行为规范,一个类不可能既继承自Animal类又继承自Runnable类。
再看代码:
public interface Runnable {
void Run();
}
public class Mechanical {
}
public class Car : Mechanical, Runnable {
public void Run() {
Console.WriteLine("汽车开走了!");
}
}
public class Animal {
}
public class Cat : Animal, Runnable {
public void Run() {
Console.WriteLine("猫跑掉了!");
}
}
我们使用了一个新的关键字interface,很容易就解决了上面的问题,用interface声明的我们称为接口。我们发现,接口中可以包含方法的声明,而所有的类除了可以继承一个超类外,还可以继承多个接口。当然,习惯上把对接口的继承不叫做继承,而叫做“实现”,即一个类实现了某些接口。
接口定义的特点:接口中定义的方法上,自动为public abstract,即公共抽象方法。接口本身也为抽象。接口中不允许以任何方式包含具有方法体的方法。
接口完美的解决了所有的问题:
- 接口中只允许包含方法声明,所以即便接口被多继承,也不会产生任何父子悖论问题;
- 接口不是实体类,不带有类别的含义,只是一种行为的规范,所以可以被不同含义的多个类同时继承;
- 接口可以赋予不同的类相同的行为准则(接口中定义的方法声明),所以接口类型变量可以引用到所有实现此接口类的实例上。
属性是一对方法,所以接口中也可以包含属性的定义。接口中不允许出现字段的定义。
看一段完整的代码,从中体会接口的特点。
public interface Runnable {
// 定义抽象属性
double Speed {
get;
}
void Run();
}
public class Mechanical {
}
public class Car : Mechanical, Runnable {
private double speed = 60.0;
public void Run() {
Console.WriteLine("汽车开走了!");
}
/// <summary>
/// 实现接口中定义的抽象属性并添加set访问器
/// </summary>
public double Speed {
get {
return this.speed;
}
set {
this.speed = value;
}
}
}
public class Animal {
}
public class Cat : Animal, Runnable {
public void Run() {
Console.WriteLine("猫跑掉了!");
}
/// <summary>
/// 实现接口中定义的抽象属性
/// </summary>
public double Speed {
get {
return 5.0;
}
}
}
除了上面讲的接口概念,C#还有一个非常有趣的特性,即接口的显式实现,接口显式实现体现了这样一种效果:一个类同时实现多个接口中的多个同名方法。例如接口IA中有个方法void Test();,接口IB也有一个方法void Test();,类C实现IA和IB接口。如下:
/// <summary>
/// 接口IA
/// </summary>
public interface IA {
/// <summary>
/// 在接口IA中定义Test方法
/// </summary>
void Test();
}
/// <summary>
/// 接口IB
/// </summary>
public interface IB {
/// <summary>
/// 在接口IB中定义Test方法(和IA中定义的Test方法一致)
/// </summary>
void Test();
}
/// <summary>
/// 定义类C,同时实现IA接口和IB接口
/// 这时候可以有三种选择:
/// 1、实现IA和IB接口定义的Test方法
/// 2、针对接口IA实现其定义的Test方法
/// 3、针对接口IB实现其定义的Test方法
/// </summary>
public class C : IA, IB {
/// <summary>
/// 实现IA和IB接口定义的Test方法
/// 以public关键字修饰,表示该方法公共
/// </summary>
public void Test() {
Console.WriteLine("OK");
}
/// <summary>
/// 指明实现IA接口中定义的Test方法,
/// 使用 接口名.方法 来指定要实现的方法是由哪一个接口定义的。
/// 显式实现的方法前无访问修饰符,表示其至少不是public的,所以无法使用C类对象直接访问
/// </summary>
void IA.Test() {
Console.WriteLine("Hello");
}
/// <summary>
/// 指明实现IB接口中定义的Test方法
/// </summary>
void IB.Test() {
Console.WriteLine("World");
}
}
/// <summary>
/// 主方法
/// </summary>
static void Main(string[] args) {
// 实例化C类的对象
C c = new C();
// 通过C类对象调用Test方法,这时调用的是以public修饰符修饰的Test实现
c.Test();
// 将C类的引用类型提升为IA类的引用类型,此时,调用的是C类中显式实现的IA.Test方法
IA a = c;
a.Test();
// 将C类的引用类型提升为IB类的引用类型,此时,调用的是C类中显式实现的IB.Test方法
IB b = c;
b.Test();
}
所以,接口的显式实现就是一个类可以显式的指定一个方法实现的是哪个接口的方法。显式实现接口的方法前无须添加访问修饰符,使用类的对象也无法直接访问(因为它不是public的),但可以通过将类对象引用类型提升为其接口类型来间接访问该方法。
注意:不到万不得已,无须使用接口的显示实现以尽量减少一个类对接口实现的复杂程度。