Fork me on GitHub
深入剖析C#的多态

一、什么是多态

  面向对象程序设计中的另外一个重要概念是多态性。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。可以把一组对象放到一个数组中,然后调用它们的方法,在这种场合下,多态性作用就体现出来了,这些对象不必是相同类型的对象。当然,如果它们都继承自某个类,你可以把这些派生类,都放到一个数组中。如果这些对象都有同名方法,就可以调用每个对象的同名方法。

  同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。多态性通过派生类重载基类中的虚函数型方法来实现。

  在面向对象的系统中,多态性是一个非常重要的概念,它允许客户对一个对象进行操作,由对象来完成一系列的动作,具体实现哪个动作、如何实现由系统负责解释。

  “多态性”一词最早用于生物学,指同一种族的生物体具有相同的特性。在C#中,多态性的定义是:同一操作作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的执行结果。C#支持两种类型的多态性:

● 编译时的多态性

  编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。

● 运行时的多态性

  运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C#中,运行时的多态性通过虚成员实现。

  编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。

二、实现多态

  多态性是类为方法(这些方法以相同的名称调用)提供不同实现方式的能力。多态性允许对类的某个方法进行调用而无需考虑该方法所提供的特定实现。例如,可能有名为 Road 的类,它调用另一个类的 Drive 方法。这另一个类 Car 可能是 SportsCar 或 SmallCar,但二者都提供 Drive 方法。虽然 Drive 方法的实现因类的不同而异,但 Road 类仍可以调用它,并且它提供的结果可由 Road 类使用和解释。

  可以用不同的方式实现组件中的多态性:

● 接口多态性。

● 继承多态性。

● 通过抽象类实现的多态性。

  接口多态性

  多个类可实现相同的“接口”,而单个类可以实现一个或多个接口。接口本质上是类需要如何响应的定义。接口描述类需要实现的方法、属性和事件,以及每个成员需要接收和返回的参数类型,但将这些成员的特定实现留给实现类去完成。

  组件编程中的一项强大技术是能够在一个对象上实现多个接口。每个接口由一小部分紧密联系的方法、属性和事件组成。通过实现接口,组件可以为要求该接口的任何其他组件提供功能,而无需考虑其中所包含的特定功能。这使后续组件的版本得以包含不同的功能而不会干扰核心功能。其他开发人员最常使用的组件功能自然是组件类本身的成员。然而,包含大量成员的组件使用起来可能比较困难。可以考虑将组件的某些功能分解出来,作为私下实现的单独接口。

  根据接口来定义功能的另一个好处是,可以通过定义和实现附加接口增量地将功能添加到组件中。优点包括:

  1.简化了设计过程,因为组件开始时可以很小,具有最小功能;之后,组件继续提供最小功能,同时不断插入其他的功能,并通过实际使用那些功能来确定合适的功能。

  2.简化了兼容性的维护,因为组件的新版本可以在添加新接口的同时继续提供现有接口。客户端应用程序的后续版本可以利用这些接口的优点。

  通过继承实现的多态性

  多个类可以从单个基类“继承”。通过继承,类在基类所在的同一实现中接收基类的所有方法、属性和事件。这样,便可根据需要来实现附加成员,而且可以重写基成员以提供不同的实现。请注意,继承类也可以实现接口,这两种技术不是互斥的。

  C# 通过继承提供多态性。对于小规模开发任务而言,这是一个功能强大的机制,但对于大规模系统,通常证明会存在问题。过分强调继承驱动的多态性一般会导致资源大规模地从编码转移到设计,这对于缩短总的开发时间没有任何帮助。

  何时使用继承驱动的多态性呢?使用继承首先是为了向现有基类添加功能。若从经过完全调试的基类框架开始,则程序员的工作效率将大大提高,方法可以增量地添加到基类而不中断版本。当应用程序设计包含多个相关类,而对于某些通用函数,这些相关类必须共享同样的实现时,您也可能希望使用继承。重叠功能可以在基类中实现,应用程序中使用的类可以从该基类中派生。抽象类合并继承和实现的功能,这在需要二者之一的元素时可能很有用。

  通过抽象类实现的多态性

  抽象类同时提供继承和接口的元素。抽象类本身不能实例化,它必须被继承。该类的部分或全部成员可能未实现,该实现由继承类提供。已实现的成员仍可被重写,并且继承类仍可以实现附加接口或其他功能。

  抽象类提供继承和接口实现的功能。抽象类不能示例化,必须在继承类中实现。它可以包含已实现的方法和属性,但也可以包含未实现的过程,这些未实现过程必须在继承类中实现。这使您得以在类的某些方法中提供不变级功能,同时为其他过程保持灵活性选项打开。抽象类的另一个好处是:当要求组件的新版本时,可根据需要将附加方法添加到基类,但接口必须保持不变。

   何时使用抽象类呢?当需要一组相关组件来包含一组具有相同功能的方法,但同时要求在其他方法实现中具有灵活性时,可以使用抽象类。当预料可能出现版本问题时,抽象类也具有价值,因为基类比较灵活并易于被修改。
示例:实现多态性的程序

using System ;

public class DrawingBase

{

public virtual void Draw( )

{

Console.WriteLine("I'm just a generic drawing object.") ;

}

}

public class Line : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Line.") ; }

}

public class Circle : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Circle.") ; }

}

public class Square : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Square.") ; }

}

public class DrawDemo

{

public static int Main(string[] args)

{

DrawingBase [] dObj = new DrawingBase [4];

dObj[0] = new Line( ) ;

dObj[1] = new Circle( ) ;

dObj[2] = new Square( ) ;

dObj[3] = new DrawingBase( ) ;

foreach (DrawingBase drawObj in dObj)

drawObj.Draw( ) ;

return 0;

}

}



  说明:上面程序演示了多态性的实现。在DrawDemo类中的Main( )方法中,创建了一个数组,数组元素是DrawingBase类的对象。该数组名为dObj,是由四个DrawingBase类型的对象组成。接下来,初始化dObj数组,由于Line,Circle和Square类都是DrawingBase类的派生类,所以这些类可以作为dObj数组元素的类型。如果C#没有这种功能,你得为每个类创建一个数组。继承的性质可以让派生对象当作基类成员一样用,这样就节省了编程工作量。 一旦数组初始化之后,接着是执行foreach循环,寻找数组中的每个元素。在每次循环中,dObj 数组的每个元素(对象)调用其Draw( )方法。多态性体现在:在运行时,各自调用每个对象的Draw( )方法。尽管dObj 数组中的引用对象类型是DrawingBase,这并不影响派生类重载DrawingBase类的虚方法Draw( )。 在dObj 数组中,通过指向DrawingBase基类的指针来调用派生类中的重载的Draw( )方法。

  输出结果是:

I'm a Line.

I'm a Circle.

I'm a Square.

I'm just a generic drawing object.

  在DrawDemo 程序中,调用了每个派生类的重载的Draw( )方法。 最后一行中,执行的是DrawingBase类的虚方法Draw( )。这是因为运行到最后,数组的第四个元素是DrawingBase类的对象。
三、虚方法

  当类中的方法声明前加上了virtual 修饰符,我们称之为虚方法,反之为非虚。使用了virtual 修饰符后,不允许再有static, abstract, 或override 修饰符。

示例1:带有虚方法的类

using System ;

public class DrawingBase

{

public virtual void Draw( )

{ Console.WriteLine("这是一个虚方法!") ; }

}



  说明:这里定义了DrawingBase类。这是个可以让其他对象继承的基类。该类有一个名为Draw( )的方法。Draw( )方法带有一个virtual修饰符,该修饰符表明:该基类的派生类可以重载该方法。DrawingBase类的 Draw( )方法完成如下事情:输出语句"这是一个虚方法!"到控制台。

示例2:带有重载方法的派生类

using System ;

public class Line : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("画线.") ; }

}

public class Circle : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("画圆.") ; }

}

public class Square : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("画正方形.") ; }

}



  说明:上面程序定义了三个类。这三个类都派生自DrawingBase类。每个类都有一个同名Draw( )方法,这些Draw( )方法中的每一个都有一个重载修饰符。重载修饰符可让该方法在运行时重载其基类的虚方法,实现这个功能的条件是:通过基类类型的指针变量来引用该类。

  对于非虚的方法,无论被其所在类的实例调用,还是被这个类的派生类的实例调用,方法的执行方式不变。而对于虚方法,它的执行方式可以被派生类改变,这种改变是通过方法的重载来实现的。

  下面的例子说明了虚方法与非虚方法的区别。
using System ;

class A

{

public void F( ) { Console.WriteLine("A.F") ; }

public virtual void G( ) { Console.WriteLine("A.G") ; }

}

class B: A

{

new public void F( ) { Console.WriteLine("B.F") ; }

public override void G( ) { Console.WriteLine("B.G") ; }

}

class Test

{

static void Main( )

{

B b = new B( ) ;

A a = b;

a.F( ) ;

b.F( ) ;

a.G( ) ;

b.G( ) ;

}

}[/quote]

  例子中,A 类提供了两个方法:非虚的F 和虚方法G 。类B 则提供了一个新的非虚的方法F, 从而覆盖了继承的F; 类B 同时还重载了继承的方法G 。那么输出应该是:A.F B.F B.G B.G

  注意到本例中,方法a.G( ) 实际调用了B.G,而不是A.G,这是因为编译时值为A,但运行时值为B ,所以B 完成了对方法的实际调用。

在派生类中对虚方法进行重载

  先让我们回顾一下普通的方法重载,普通的方法重载指的是:类中两个以上的方法(包括隐藏的继承而来的方法),取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用哪个方法。

  而对基类虚方法的重载是函数重载的另一种特殊形式。在派生类中重新定义此虚函数时,要求的是方法名称,返回值类型、参数表中的参数个数、类型顺序都必须与基类中的虚函数完全一致。在派生类中声明对虚方法的重载,要求在声明中加上override 关键字,而且不能有new, static 或virtual 修饰符。

  看一个用汽车类的例子来说明多态性的实现的程序:

using System ;

class Vehicle//定义汽车类

{

public int wheels; //公有成员轮子个数

protected float weight; //保护成员重量

public Vehicle(int w,float g)

{

wheels = w;

weight = g;

}

public virtual void Speak( )

{

Console.WriteLine( " the w vehicle is speaking!" ) ;

}

};

class Car:Vehicle //定义轿车类

{

int passengers; //私有成员乘客数

public Car(int w,float g,int p) : base(w,g)

{

wheels = w;

weight = g;

passengers = p;

}

public override void Speak( )

{

Console.WriteLine( " The car is speaking:Di-di!" ) ;

}

}

class Truck:Vehicle //定义卡车类

{

int passengers; //私有成员乘客数

float load; //私有成员载重量

public Truck (int w,float g,int p, float l) : base(w,g)

{

wheels = w;

weight = g;

passengers = p;

load = l;

}

public override void Speak( )

{

Console.WriteLine( " The truck is speaking:Ba-ba!" ) ;

}

public static void Main( )

{

Vehicle v1 = new Vehicle(0,0 ) ;

Car c1 = new Car(4,2,5) ;

Truck t1 = new Truck(6,5,3,10) ;

v1.Speak( ) ;

v1 = c1;

v1.Speak( ) ;

c1.Speak( ) ;

v1 = t1;

v1.Speak( ) ;

t1.Speak( ) ;

}

}



  分析上面的例子我们看到:

● Vehicle 类中的Speak 方法被声明为虚方法,那么在派生类中就可以重新定义此方法。

● 在派生类Car 和Truck 中分别重载了Speak 方法,派生类中的方法原型和基类中的方法原型必须完全一致。

● 在Test 类中,创建了Vehicle 类的实例v1, 并且先后指向Car 类的实例c1 和Truck 类的实例t1。

  运行该程序结果应该是:

The Vehicle is speaking!

The car is speaking:Di-di!

The car is speaking:Di-di!

The truck is speaking:Ba-ba!

The truck is speaking:Ba-ba!

  这里,Vehicle 类的实例v1 先后被赋予Car 类的实例c1, 以及Truck 类的实例t1的值。在执行过程中,v1 先后指代不同的类的实例,从而调用不同的版本。这里v1 的Speak 方法实现了多态性,并且v1.Speak 究竟执行哪个版本,不是在程序编译时确定的,而是在程序的动态运行时,根据v1 某一时刻的指代类型来确定的,所以还体现了动态的多态性。
四、接口多态性

  多个类可实现相同的“接口”,而单个类可以实现一个或多个接口。接口本质上是类需要如何响应的定义。接口描述类需要实现的方法、属性和事件,以及每个成员需要接收和返回的参数类型,但将这些成员的特定实现留给实现类去完成。

  组件编程中的一项强大技术是能够在一个对象上实现多个接口。每个接口由一小部分紧密联系的方法、属性和事件组成。通过实现接口,组件可以为要求该接口的任何其他组件提供功能,而无需考虑其中所包含的特定功能。这使后续组件的版本得以包含不同的功能而不会干扰核心功能。

  其他开发人员最常使用的组件功能自然是组件类本身的成员。然而,包含大量成员的组件使用起来可能比较困难。可以考虑将组件的某些功能分解出来,作为私下实现的单独接口。

  根据接口来定义功能的另一个好处是,可以通过定义和实现附加接口增量地将功能添加到组件中。优点包括:

● 简化了设计过程,因为组件开始时可以很小,具有最小功能;之后,组件继续提供最小功能,同时不断插入其他的功能,并通过实际使用那些功能来确定合适的功能。

● 简化了兼容性的维护,因为组件的新版本可以在添加新接口的同时继续提供现有接口。客户端应用程序的后续版本可以利用这些接口的优点(如果这样做有意义)。

五、继承多态性

  多个类可以从单个基类“继承”。通过继承,类在基类所在的同一实现中接收基类的所有方法、属性和事件。这样,便可根据需要来实现附加成员,而且可以重写基成员以提供不同的实现。请注意,继承类也可以实现接口,这两种技术不是互斥的。

  C# 通过继承提供多态性。对于小规模开发任务而言,这是一个功能强大的机制,但对于大规模系统,通常证明会存在问题。过分强调继承驱动的多态性一般会导致资源大规模地从编码转移到设计,这对于缩短总的开发时间没有任何帮助。看下面的例子:

class B

{ public virtual void foo () {} }

class D : B

{

public override void foo () {}

}

//试图重载一个非虚的方法将会导致一个编译时错误,除非对该方法加上“new”关键字,//以指明该方法意欲隐藏父类的方法。

class N : D

{

public new void foo () {}

public static void Main() {

N n = new N ();

n.foo( ) ; // 调用N的foo

((D)n).foo( ) ; // 调用D的foo

((B)n).foo( ) ; // 调用D的foo

}

}



  和C++、Java相比,C#的override关键字使得阅读源代码时可以清晰地看出哪些方法是重载的。不过,使用虚方法有利有弊。第一个有利点是:避免使用虚方法轻微的提高了执行速度。第二点是可以清楚地知道哪些方法会被重载。

  我们看一个关于飞机描述的类。假设我们有一个描述飞机的基类。现在,我们要完成一个飞机控制系统,有一个全局的函数fly,它负责让传递给它的飞机起飞,那么,只需要这样:

using System ;

class plane

{

public virtual void fly(){} //起飞纯虚函数

public virtual void land() {} //着陆纯虚函数

public virtual string modal(){} //查寻型号纯虚函数

}

// 然后,我们从plane派生出两个子类,直升机(copter)和喷气式飞机(jet):

class copter:plane

{

private String fModal ;

public override void fly(){}

public override void land(){}

public override string modal(){}

}

class jet : plane

{

private String fModal ;

public override void fly(){}

public override void land(){}

public override string modal{}

}



  就可以让所有传给它的飞机(plane的子类对象)正常起飞!不管是直升机还是喷气机,甚至是现在还不存在的,以后会增加的飞碟。因为,每个子类都已经定义了自己的起飞方式。

  可以看到 plane.fly()函数接受参数的是 plane类对象引用,而实际传递给它的都是 plane的子类对象,多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。 很显然,parent = child; 就是多态的实质!因为直升机“是一种”飞机,喷气机也“是一种”飞机,因此,所有对飞机的操作,都可以对它们操作,此时,飞机类就作为一种接口。多态的本质就是将子类类型的指针赋值给父类类型的指针(在OP中是引用),只要这样的赋值发生了,多态也就产生了,因为实行了“向上映射”。

posted on 2010-08-17 16:34  HackerVirus  阅读(169)  评论(0编辑  收藏  举报