多态
Polymorphism
继承有两个功能强大的方面。其一是代码重用。创建ListBox类时,可以重用基类(Control)中的某些逻辑。
另一方面更为强大,这就是多态性(polymorphism)。英文中“Poly”意为多,而“morph”意为形态。因此,多态性指的是可以使用一个类型的多种形式,而无需考虑细节。
当电话公司向某个电话发送一个铃声信号时,它并不知道电话线另一头是什么类型的电话。也许是老式的手摇电话,要自己生电响铃,也许是可以演奏数字音乐的电子电话。
电话公司只知道“基类型”Phone,它希望这种类型的任何“实例”都知道如何响铃。当电话公司让电话响铃时,它只要求电话“正确响铃”。电话公司对电话的这种方式就是多态性的体现。
创建多态类型
Creating Polymorphic Types
因为ListBox是一种is-a Control,Button也是一种is-a Control,我们希望在任何要求Control的情况下都能用这些类型。例如,一个表单想保存它所管理的所有Control实例的集合,这样在表单打开时,可以让自己所有Control自我绘制。对于此操作,表单并不需要知道哪个元素是列表框,哪些是按钮,它只需把集合遍历一次,让它们自己画就行了。简而言之,表单多态地处理Control对象。
创建多态方法
Creating Polymorphic Methods
要创建支持多态性的方法,只需在其基类中将其标记为virtual。例如,要指明例5-1中Control类的DrawWindow()方法是多态的,只需如下在声明中加上关键字virtual:
public virtual void DrawWindow()
现在每个派生类均可自由地实现自己版本的DrawWindow()。为此,在派生类方法定义中用关键字 override覆盖基类的虚方法,然后在已覆盖方法中添上新代码。
下面是例5-1的片段(稍后会见到完整的),其中ListBox从Control派生,并实现了自己版本的DrawWindow():
public override void DrawWindow()
{
base.DrawWindow(); // 调用基方法
Console.WriteLine ("Writing string to the listbox: {0}",
listBoxContents);
}
关键字 override的作用是告诉编译器,此类有意地重新定义了DrawWindow()的工作方式。同样,在另一个由Control派生的类Button中,也要覆盖这一方法。
在例5-1的主体中,首先创建三个对象,一个Control,一个ListBox,一个Button。然后调用每个对象的DrawWindow():
Control win = new Control(1,2);
ListBox lb = new ListBox(3,4,"Stand alone list box");
Button b = new Button(5,6);
win.DrawWindow();
lb.DrawWindow();
b.DrawWindow();
正如我们所愿,每个对象都将调用正确的DrawWindow()方法。到目前这止,还没有出现什么多态性。真正的魔术从创建一个Control对象数组开始。因为ListBox是一个is-a Control,可以自由地把ListBox放入Control数组。同理也可以把Button放入Control对象数组:
Control[] winArray = new Control[3];
winArray[0] = new Control(1,2);
winArray[1] = new ListBox(3,4,"List box in array");
winArray[2] = new Button(5,6);
当我们对其中每个对象调用DrawWindow()时会发生什么呢?
for (int i = 0;i < 3; i++)
{
winArray[i].DrawWindow();
}
所有编译器都知道,有三个Control对象,而且每个都要调用DrawWindow()。如果没有将该方法标记为virtual,该方法将调用三次。但是,由于该方法已标记为virtual,且派生类对其进行了重定义,当对数组调用方法时,编译器会确定实际对象(一个Control,一个ListBox和一个Button)的运行期类型,并为此调用正确的方法。这正是多态性的实质。完整的代码如例5-1所示。
提示:代码中用到了数组,也就是同类型对象的一种集合,可以用索引操作符访问数组成员:
//设置偏移为5
//的元素的值
MyArray[5] = 7;
数组的第一个元素的索引为0。本例中数组的使用并不是特别直观。我们将在第九章详细阐述数组。
示例5-1:使用虚方法
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
#endregion
namespace VirtualMethods
{
public class Control
{
// 这些方法是保护的
// 因此只对派生类的方法可见
// 本章的后面我们再来讨论这一问题
protected int top;
protected int left;
// 构造方法有两个整型参数
// 以确定控制台中的位置
public Control( int top, int left )
{
this.top = top;
this.left = left;
}
//模拟绘制窗口
public virtual void DrawWindow()
{
Console.WriteLine( "Control: drawing Control at {0}, {1}",
top, left );
}
}
// ListBox派生自 Control
public class ListBox : Control
{
private string listBoxContents; // 新的成员变量
// 构造方法添加一个参数
public ListBox(
int top,
int left,
string contents ):
base(top, left) // 调用基构造方法
{
listBoxContents = contents;
}
// 重定义版本(注意关键字)
// 因为在派生方法中我们改变了行为
示例5-1:使用虚方法(续例)
public override void DrawWindow()
{
base.DrawWindow(); // 调用基方法
Console.WriteLine( "Writing string to the listbox: {0}",
listBoxContents );
}
}
public class Button : Control
{
public Button(
int top,
int left ):
base(top, left)
{
}
// 重新定义的版本(注意关键字)
// 因为在派生方法中改变了行为
public override void DrawWindow()
{
Console.WriteLine( "Drawing a button at {0}, {1}\n",
top, left );
}
}
public class Tester
{
static void Main()
{
Control win = new Control( 1, 2 );
ListBox lb = new ListBox( 3, 4, "Stand alone list box" );
Button b = new Button( 5, 6 );
win.DrawWindow();
lb.DrawWindow();
b.DrawWindow();
Control[] winArray = new Control[3];
winArray[0] = new Control( 1, 2 );
winArray[1] = new ListBox( 3, 4, "List box in array" );
winArray[2] = new Button( 5, 6 );
for ( int i = 0; i < 3; i++ )
{
winArray[i].DrawWindow();
}
}
}
}
示例5-1:使用虚方法(续例)
输出
Control: drawing Control at 1, 2
Control: drawing Control at 3, 4
Writing string to the listbox: Stand alone list box
Drawing a button at 5, 6
Control: drawing Control at 1, 2
Control: drawing Control at 3, 4
Writing string to the listbox: List box in array
Drawing a button at 5, 6
注意整个例子中,都用关键字 override标记了重新定义的方法:
public override void DrawWindow()
编译器现在就知道,要按多态性原则处理对象,使用重新定义的方法。编译器负责跟踪对象的真实类型,以及处理“迟绑定”,这样在Control引用实际上指向ListBox对象时,调用的是ListBox.DrawWindow()。
提示:C++程序员注意:必须显式用关键字 override标记重新定义了虚方法的方法声明。
调用基类构造方法
Calling Base Class Constructors
在示例5-1中,新的类ListBox 派生自 Control ,有自己的构造方法,参数为三个整数。 ListBox 构造方法通过在参数列表之后放一个冒号,然后用base关键字调用基类,来调用其父类(Control)的构造方法:
public ListBox(
int theTop,
int theLeft,
string theContents):
base(theTop, theLeft) // 调用基类构造方法
因为类不能继承构造方法,派生类必须实现自己的构造方法,只能通过显式调用来使用基类的构造方法。
如果基类有可以访问的默认构造方法,派生类的构造方法就不需要显式调用基类构造方法了,将隐式地调用默认构造方法。但是,如果基类没有默认构造方法,所有派生类的构造方法都必须使用base关键字显式调用一个基类构造方法。
提示:第4章中讨论过,如果不声明任何构造方法,编译器会为我们创建一个默认构造方法。无论是自己编写一个,还是由编译器默认提供一个,默认构造方法都是没有参数的。但是请注意,如果创建了某一种构造方法(无论有无参数),编译器就不会创建默认构造方法了。
控制访问
Controlling Access
类及其成员的可见性是用访问修饰符进行限制的,修饰符包括 public、private、protected、internal和protected internal。(参见第4章中对访问修饰符的讨论。)
我们已经看到, public可以使成员被其他类的成员方法访问,而 private 表示成员只对自己类的成员方法可见。 protected 关键字将可见性扩展到派生类,而 internal 将可见性扩展到同一程序集的所有类的方法。(注1)
internal protected 关键字对允许访问同一程序集(internal)或者派生类(protected)中的成员。可以看成是 internal 或 protected。
类及其成员可以指定为所有这些访问级别。如果一个类的成员的访问级别指定为与类的不同,则应用更严格的级别。因此,如果定义了一个类myClass,如下所示:
public class myClass
{
// ...
protected int myValue;
}
myValue的可访问性是保护的,而类本身是公共的。公共类就是所有其他类都可以看到的类。经常,创建类的目的只是辅助程序集中的其他类,这些类应该被标记为 internal 而不是public。
用关键字new和override进行版本处理
Versioning with the new and override Keywords
在C#中,程序员是否重定义虚方法是用override关键字显式声明的。这有助于发布代码
的新版本,对基类的改变不会破坏派生类中的已有代码。要求必须使用关键字override,有助于防止此类问题。
那么到底是怎样的呢?假设前一个例子中Control基类是A公司写的。而ListBox和RadioButton类是由B公司的程序员用从A公司买来的Control基类写成的。B公司的程序员对基类的设计几乎无法控制,这包括A公司将来可能选择的改动。
现在假设B公司的一个程序员要在ListBox中加一个方法Sort():
public class ListBox : Control
{
public virtual void Sort() {...}
}
如果A公司发布的Control类的第二版中,也在其Control公共类中加入了一个方法Sort(),问题就来了:
public class Control
{
// ...
public virtual void Sort() {...}
}
在其他面向对象语言(如C++)中,Control中新的虚方法Sort()现在是ListBox中虚方法Sort()的基方法。编译器在我们想调用Control中的Sort()方法时,会去调用ListBox的Sort()方法。在Java中,如果Control中的Sort()返回类型不同,类装载器会认为ListBox中的Sort()是非法的重定义,装载会失败。
C#有效地防止了这种混乱情况。C#中,虚方法总是被当成虚拟分派(dispatch)的根;也就是说,一旦C#发现虚方法,它不会再去查看继承层次。如果Control中加入一个新的虚方法Sort(),ListBox的运行期行为保持不变。
但当ListBox再次编译时,编译器会发出如下警告:
...\class1.cs(54,24): warning CS0114: 'ListBox.Sort( )' hides
inherited member 'Control.Sort( )'.
To make the current member override that implementation,
add the override keyword. Otherwise add the new keyword.
(…\class|.cs(54,24):警告CS01114:'ListBox.Sort()'隐藏了
被继承的成员'Control.Sort()'。
要使当前成员重定义此实现,
请添加override 关键字。否则加上new 关键字。)
要避免出现这种警告,程序员必须指明自己的意图。他可以将ListBox的Sort()方法标记为new,以指明它不重定义Control中的虚方法:
public class ListBox : Control
{
public new virtual void Sort() {...}
这样警告就不会出现。但如果程序员确实要重定义Control中的方法,只需用override 关键字明确表明:
public class ListBox : Control
{
public override void Sort() {...}
警告:有人可能会在所有虚方法上都加上new 关键字,以避免这种警告出现。但这很不好。当new出现在代码中时,应该是一种版本控制的文档记号。它会向基类的潜在用户指出,哪些不应该重定义。不加选择地滥用new,会削弱这种文档作用。而且警告的存在也能帮助我们发现真正的问题。






