首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

多态

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。

用关键字newoverride进行版本处理

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,会削弱这种文档作用。而且警告的存在也能帮助我们发现真正的问题。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论