第11章 多态
在理解C#多态性之前,首先理解一下什么是多态。多态是面向对象编程的重要概念,是面向对象程序设计(OOP[1])中的三大机制之一。因为有了继承,一个类可以从类继承,可以实现接口,那么这个类除了有它本身的类型,还将具有基类的类型,以及它所实现的接口的类型,也就意味着一个基类的引用可以指向它的所有派生类型,那么当通过基类的引用调用在派生类中实现的方法时,不同的派生类将产生不同的调用结果,这就是多态性。可见继承机制是实现多态的基础。C#中的每种类型都是多态的,因为任何类型都从Object类型派生。
举个简单的例子说明,假如有一个数组,其中有3个元素B、C、D,它们都有共同的基类A,在基类中有一个方法M,这些派生类也都有一个方法M的实现,当我们调用这3个元素的M方法时,将产生3种不同的结果,这就是多态,如图11-1所示。
图 11-1 多态
C#中的多态性分为两种,一种是编译时的多态性,一种是运行时的多态性。
❑编译时的多态性:编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数个数、参数类型等信息决定使用哪个重载方法,以实现某种操作。
❑运行时的多态性:C#中运行时的多态性是通过覆写虚方法实现的。具体是指直到系统运行时,才根据实际情况决定实现何种操作。
下面我们来分别理解一下C#多态中涉及的几个概念:重载方法、虚方法、覆写方法和抽象类及抽象方法。
11.1 重载方法
在8.9节我们已经介绍过方法的重载,本章将进一步学习这一重要概念。我们知道,重载就是在同一个类中存在多个同名的方法,但签名却不同。因此,方法的重载需要以下几个前提条件:
❑在同一个类中;
❑方法名相同;
❑方法签名不同。
需要特别注意的是方法签名,方法的签名包括方法的名称以及参数信息(包括形参的修饰符、数目、类型以及泛型参数的数目)。特别需要注意的是,返回值类型、形参和类型参数的名称并不属于方法签名的一部分。
重载是编译时的多态,事实上是一种静态绑定。
以上是针对非泛型方法的重载,而对于自.NET 2.0便开始引入的泛型技术[2],泛型类的方法重载如代码清单11-1所示:
代码清单11-1 泛型类的方法重载
1 using System;
2 public class MethodSample
3{
4 public static void Main()
5{
6 TestClass<int,string>tc=new TestClass<int,string>();
7 tc.DoSomething(5,"10");
8 tc.DoSomething("10",5);
9
10 TestClass<int,int>tc2=new TestClass<int,int>();
11 tc2.DoSomething(5,10);
12 tc2.DoSomething(10,5);
13}
14}
15
16 public class TestClass<T,V>
17{
18 public T DoSomething(T n,V m)//泛型方法
19{
20 Console.WriteLine(“方法1”);
21 return n;
22}
23
24 public T DoSomething(V n,T m)//泛型方法
25{
26 Console.WriteLine(“方法2”);
27 return m;
28}
29
30 public int DoSomething(int m,int n)//非泛型方法
31{
32 Console.WriteLine(“方法3”);
33 return 0;
34}
35}
上述代码定义了一个具有两个泛型类型参数(T和V)的泛型类TestClass,其中定义了两个泛型方法和一个非泛型方法。
先来观察下第6行至第8行,分别使用int和string作为类型参数实例化TestClass类,那么,此时两个泛型方法其实为代码清单11-2所示。
代码清单11-2 使用int和string作为类型参数实例化TestClass类
public int DoSomething(int n,string m)
{
Console.WriteLine(“方法1”);
return n;
}
public int DoSomething(string n,int m)
{
Console.WriteLine(“方法2”);
return m;
}
这样,即演变成非泛型方法的重载,如图11-2所示。
图 11-2 使用int、string作为类型参数实例化TestClass类
但是,如果像第10~12行那样又如何呢?使用两个int作为类型参数实例化TestClass类,此时,TestClass类见代码清单11-3和图11-3。
代码清单11-3 使用两个int作为类型参数实例化TestClass类
public class TestClass<int,int>
{
public int DoSomething(int n,int m)
{
Console.WriteLine(“方法1”);
return n;
}
public int DoSomething(int n,int m)
{
Console.WriteLine(“方法2”);
return m;
}
public int DoSomething(int m,int n)
{
Console.WriteLine(“方法3”);
return 0;
}
}
图 11-3 T、V均使用int作为类型参数
根据重载的定义,这三个方法的方法签名完全一样,是否造成了冲突呢?编译一下代码清单11-1,结果一切正常,CLR并没有如预期般抛出异常,这是怎么回事呢?这是因为有第3个非泛型方法存在(代码清单11-1的第30行),如果删掉第3个非泛型重载方法,或者使用两个string作为类型参数,如图11-4所示,系统将抛出如下异常:
在以下方法或属性之间的调用不明确:
“ProgrammingCSharp4.TestClass<T,V>.DoSomething(T,V)”和
“ProgrammingCSharp4.TestClass<T,V>.DoSomething(V,T)”
即,在两个方法之间发生混淆,因为这两个方法的签名相同,运行时无法在第一个和第二个之间做出选择。我们由此可知:
❑当非泛型方法和泛型方法具有相同的签名时,运行时将优先调用非泛型方法;
❑泛型方法的重载是否产生混淆,是在运行时调用的时候检查的,而非编译的时候检查。
图 11-4 泛型方法重载中发生的混淆
[1]OOP:Object Oriented Programming,面向对象的程序设计。
[2]泛型将在第17章介绍,请读者自行参阅。