在本文,我们将通过一个灵活的绘图应用程序提供一个有关继承、abstract (MustInherit) 基类和接口的更为完整的示例。这不是一个控制台应用程序;由于其图形化的特征,更适合作为一个 Microsoft Windows 窗体应用程序。(这就给了我们一个了解 Windows 窗体的机会。)
该 ASP.NET 版本将演示如何在 Web 页上使用自定义绘制的位图 -- 这在大多数 Web 编程系统中是非常难以实现的,但使用 ASP.NET 则很简单。Dr. GUI 相信您会喜欢这一点。而且您还可以运行该应用程序。
经典的多态示例
在教授编程时,有一些常用的、非常标准的示例程序。而我最初曾发誓不使用这些示例:我不会使用一个字符串类作为示例,也不会使用复杂的数字或绘图应用程序。毕竟,这样做就不是原创了。
然而随着事情的发展,使用这些示例显得很有必要(不仅仅是因为懒惰):这些示例非常丰富,易于解释和理解,并且可以非常清晰地揭示核心概念。
以下是该程序 Windows 窗体版本的屏幕快照:
图 1:经典多态示例的 Windows 窗体版本
以下是 ASP.NET 版本在浏览器中的显示:
图 2:经典多态示例的 ASP.NET 版本
您可以运行上面显示的 ASP.NET 版本。
我们的任务
这个程序的基本思想如下:我们有一个 abstract 基类(在 Microsoft Visual Basic? 中是 MustInherit),其中包含公共数据(如边框)和一套虚拟方法,虚拟方法多数是抽象的(在 Visual Basic 中是 MustOverride),例如 Draw。请注意,Draw 的多态性很重要,因为每个可绘制对象类型(如点、线、矩形、圆等)都是用完全不同的代码绘制的。
虽然方法可以是多态的,但数据不能。因此,我们只将确实应用于所有可能的可绘制对象的数据放在程序中 -- 在本例中,放置了一个边框和颜色(在其中绘制对象的线)。
与特定类型的可绘制对象相关的数据(例如圆的中心和半径、矩形相对点的坐标,或者一条线的端点)都应该在与该类型的可绘制对象对应的特定类(从抽象基类中派生)中声明。请注意,可以使用二次派生合并相似的对象。例如,可以从椭圆中派生出圆,因为所有的圆都是椭圆。与此类似,也可以从矩形中派生出方形,因为所有的方形都是矩形(也都是四边形、多边形)。所选择的派生树会反映类之间的关系,以及常用的预期使用模式,这样您经常执行的操作便会非常快速、方便。
以下是我们的类派生图:
图 3:类派生图
因为构造函数(在 Visual Basic 中为 New)存在的主要原因是用于初始化数据,因此构造函数不是(实际上也不能是)多态的。这意味着初始创建操作不能是多态的,因为数据要求随类型的不同而不同。但是,一个好的设计在对象创建后,可在之后的使用中将对象作为多态处理,这里我们就是这样做的。
让我们看看这个类集中包含什么,从根抽象基类开始:
抽象 (MustInherit) 基类
以下是 C# 中抽象基类的代码。单击此处在新窗口中查看全部源文件。
C# public abstract class DShape { public abstract void Draw(Graphics g); protected Rectangle bounding; protected Color penColor; // 还应具有属性 // 还应具有移动、调整大小等方法。 } |
以下是等同的 Visual Basic .NET 代码。单击此处在新窗口中查看全部源文件。
Visual Basic .NET
Public MustInherit Class DShape Public MustOverride Sub Draw(ByVal g As Graphics) Protected bounding As Rectangle Protected penColor As Color @# 还应具有属性 @# 还应具有移动、调整大小等方法。 End Class |
语法虽然不同,但很明显这是相同的类。
请注意,Draw 方法被暗示为 virtual (Overridable),因为它被声明为 abstract (MustOverride)。还要注意在这个类中我们并没有提供一个实现。因为我们尚不知道在这个类中执行的对象,因此不可能写出绘图代码。
包含哪些数据?
另请注意,这里并没有很多数据 -- 但我们已经为这样一个抽象类声明了所有数据。
每一个可绘制对象(无论其形状如何)都有一个边框 -- 即可以完全包含该对象的最小可能矩形。边框用于绘制点(作为很小的矩形)、长方形和圆 -- 并且对于其他形状,可以作为第一个用于点击或碰撞测试的快速估计。
适用于所有对象的其他共同点并没有很多;中心对于某些对象有用,例如圆和长方形,对于其他对象(如三角形)则没有意义。并且通常都是使用角来表示矩形,而不是使用中心。但您不能使用角来指定圆,因为圆没有角。Dr. GUI 确信您已经看到了为一个普通可绘制对象指定其他数据的困难之处。
每个可绘制对象还有一个与绘制它的线相关联的颜色,这里我们也做了声明。
某些派生类
如上所述,我们不能真正创建一个抽象基类类型的对象,虽然我们可以将从抽象基类(或任何基类)中派生的任何对象作为基类对象处理。
所以,为创建一个绘图对象,我们必须从抽象基类中派生一个新类 -- 并确保覆盖所有 abstract/MustOverride 方法。
在本例中我们将使用 DHollowCircle 类。DHollowRectangle 类和 DPoint 类非常相似。
以下是 C# 中的 DHollowCircle。单击此处在新窗口中查看其他类。
C#
public class DHollowCircle : DShape public override void Draw(Graphics g) { |
以下是等同的 Visual Basic .NET 类。单击此处在新窗口中查看其他类。
Visual Basic .NET
Public Class DHollowCircle Public Sub New(ByVal p As Point, ByVal radius As Integer, _ Public Overrides Sub Draw(ByVal g As Graphics) |
请注意,我们没有为这个类声明其他数据 -- 它给出的边框和笔已经足够了。(对于点和矩形是这样,但对于三角形和其他多边形就不够了。)我们的应用程序不需要在设置圆后知道圆的中心或半径,因此将它们忽略掉。(如果需要中心和半径,我们可以存储这些数据,或者根据边框计算得出。)
但我们确实需要边框,因为它是用于绘制圆的 Graphics.DrawEllipse 方法的一个参数。因此我们根据在构造函数中传递的中心点和半径计算边框。
下面我们深入了解每一个方法。
绘图如何改变
您会注意到,Draw 方法与基类基本相同 -- 主要差别在于它调用了 Fill 方法,因为要完成绘制一个填充对象,所以需要对其进行填充。我们没有为绘制轮廓重写代码,而是再次调用了基类的方法:Visual Basic .NET 中的 MyBase.Draw(g) 或 C# 中的 base.Draw(g);。
因为我们正在指派用于绘制轮廓的笔,因此需要使用 using 或 Try/Finally 和 Dispose 以确保迅速释放 Windows 笔对象。(同样,如果非常确信所调用的方法不会引发异常,可以在完成笔的处理后,跳过异常处理,而只调用 Dispose。但我们必须调用 Dispose,无论是直接调用,还是通过 using 语句。
实现 Fill 方法
Fill 方法很简单:指派一个画笔,然后在屏幕上填充对象 -- 并确保 Dispose 画笔。
请注意,在 Visual Basic .NET 中,您必须明确指定实现一个接口的方法 (... Implements IFillable.Fill);而在 C# 中,实现接口中的方法或属性由方法或属性的签名确定(因为您编写了一个称为 Fill 的方法,该方法不返回任何内容并接受一个 Graphics,因此它必须是 IFillable.Fill 的实现)。非常奇怪,Dr. GUI 通常喜欢简洁的编程结构(如果不可能通过简单的编写完成),但实际上却倾向使用 Visual Basic 的语法,因为这种语法既清晰又灵活(Visual Basic 实现类中的方法名称不必与接口中的名称匹配,并且一个给定方法通常能够实现多个接口方法)。
实现属性
IFillable 接口还包含一个属性,从中可以 set 和 get 画笔颜色。(我们在 Change fills to hot pink [将填充色更改为粉红] 按钮处理程序中使用该属性。)
为实现公开属性,我们需要一个私有或保护的字段。这里我们选择了保护字段,以便能够方便地从派生类(而不允许任何类)对其进行访问。
具有该字段后,我们可以轻松地编写一个很简单的 set 和 get 方法对以实现属性。
请再次注意,在 Visual Basic .NET 中,必须明确指定所实现的属性。
接口还是抽象 (MustInherit) 基类?
在面向对象的编程中,最常见的争论之一就是,是使用抽象基类还是使用接口。
接口可以提供一些额外的灵活性,但也要付出一定代价:对于实现该接口的每一个类,必须实现其中的所有内容。我们可以使用一个 helper 类来协助这项工作(稍后会提供一个相关示例),但您仍然必须在所有地方实现所有内容。并且接口不能包含数据(虽然如此,与在 Brand J 的系统中不同,它们可以包含属性,因此它们可以看起来好象包含了数据)。
在本例中,Dr. GUI 为 DShape 选择了使用一个抽象基类而不是一个接口,因为他不想在每个类中将数据作为属性重复实现。此外,还因为从 DShape 派生出的所有内容都是形状,由于可填充对象仍然是形状,因而也可以进行填充。
您的选择可能有所不同,但 Dr. GUI 认为他在此做出的选择非常正确。
绘图对象的容器
因为要重复绘制我们的对象(在 Windows 窗体版本中,每次都将绘制图像;在 ASP.NET 版本中,每次都将重新加载 Web 页),因此需要将它们放在一个容器中,以便能够反复访问它们。
Dr. GUI 更进一步,将容器变得智能化,使其知道如何绘制所包含的对象。以下是这个容器类的 C# 代码:
C#
public class DShapeList { public void Add(DShape d) { public void DrawList(Graphics g) { public IFillable[] GetFilledList() { |
以下为等同类的 Visual Basic .NET 代码:
Visual Basic .NET Public Class DShapeList Dim wholeList As New ArrayList() Dim filledList As New ArrayList() Public Sub Add(ByVal d As DShape) Public Sub DrawList(ByVal g As Graphics) Public Function GetFilledList() As IFillable() |