请您检查作为 Microsoft ASP.NET 应用程序运行的示例(带有源代码)。或者仅在新窗口中查看源代码。请注意,程序员的注释在示例程序中是英文的,而在本文中被翻译成中文,以便更好地解释该程序。另外,使用了此新功能后(在此感谢 MSDN Web Publishing Team!),您可以将两个窗口都放在屏幕上,这样便可以方便地查看相应代码。
简介
在本文,我们将通过一个灵活的绘图应用程序提供一个有关继承、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 的多态性很重要,因为每个可绘制对象类型(如点、线、矩形、圆等)都是用完全不同的代码绘制的。
虽然方法可以是多态的,但数据不能。因此,我们只将确实应用于所有可能的可绘制对象的数据放在程序中 -- 在本例中,放置了一个边框和颜色(在其中绘制对象的线)。
与特定类型的可绘制对象相关的数据(例如圆的中心和半径、矩形相对点的坐标,或者一条线的端点)都应该在与该类型的可绘制对象对应的特定类(从抽象基类中派生)中声明。请注意,可以使用二次派生合并相似的对象。例如,可以从椭圆中派生出圆,因为所有的圆都是椭圆。与此类似,也可以从矩形中派生出方形,因为所有的方形都是矩形(也都是四边形、多边形)。所选择的派生树会反映类之间的关系,以及常用的预期使用模式,这样您经常执行的操作便会非常快速、方便。
因为构造函数(在 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 DHollowCircle(Point p, int radius, Color penColor) {
p.Offset(-radius, -radius); // 需要转换到左上角
int diameter = radius * 2;
bounding = new Rectangle(p, new Size(diameter, diameter));
this.penColor = penColor;
}
public override void Draw(Graphics g) {
using (Pen p = new Pen(penColor)) {
g.DrawEllipse(p, bounding);
}
}
}
以下是等同的 Visual Basic .NET 类。单击此处在新窗口中查看其他类。
Visual Basic .NET
Public Class DHollowCircle
Inherits DShape
Public Sub New(ByVal p As Point, ByVal radius As Integer, _
ByVal penColor As Color)
p.Offset(-radius, -radius) ' 需要转换到左上角
Dim diameter As Integer = radius * 2
bounding = New Rectangle(p, New Size(diameter, diameter))
Me.penColor = penColor
End Sub
Public Overrides Sub Draw(ByVal g As Graphics)
Dim p = New Pen(penColor)
Try
g.DrawEllipse(p, bounding)
Finally
p.Dispose()
End Try
End Sub
End Class
请注意,我们没有为这个类声明其他数据 -- 它给出的边框和笔已经足够了。(对于点和矩形是这样,但对于三角形和其他多边形就不够了。)我们的应用程序不需要在设置圆后知道圆的中心或半径,因此将它们忽略掉。(如果需要中心和半径,我们可以存储这些数据,或者根据边框计算得出。)
但我们确实需要边框,因为它是用于绘制圆的 Graphics.DrawEllipse 方法的一个参数。因此我们根据在构造函数中传递的中心点和半径计算边框。
下面我们深入了解每一个方法。
构造函数
构造函数传递三个参数:包含圆的中心坐标的点、圆的半径以及一个 System.Drawing.Color 结构(包含用于绘制圆轮廓的颜色)。
然后我们根据中心和半径计算边框,并将笔颜色项设置为我们传递的颜色对象。
绘图代码
Draw 方法重载实际上非常简单:它根据我们保存在构造函数中的颜色对象创建一个笔对象,然后使用该笔,调用 Graphics.DrawEllipse 方法绘制圆,同时传递了我们早先创建的边框。
图形、笔和画笔
这里我们需要解释一下 Graphics、Pen 和 Brush 对象。(在开始填充我们的可填充对象时,就会看到 Brush 对象。)
Graphics 对象代表一个与某个真实绘图空间相关联的虚拟化的绘图空间。虚拟化是指,通过在 Graphics 对象上绘图,我们可以使用相同的 Graphics 方法在与该对象相关联的任何类型的实际表面上绘图。对于那些习惯于使用 MFC 或 Microsoft Windows? SDK 编程的用户,Graphics 对象相当于 Windows 中称为“设备上下文”(或 DC)的 .NET 版本。
在此 Windows 窗体应用程序中,传递到 Draw 方法的 Graphics 对象将与屏幕上的一个窗口相关联 -- 这里是 PictureBox。在我们的 Microsoft ASP.NET 应用程序中使用这一代码时,传递给 Draw 方法的 Graphics 对象将与一个位图图像相关联。它也可以和打印机或其他设备相关联。
这个方案的优点是我们可以使用相同的绘图代码在不同的表面上绘图。在我们的绘图代码中,我们不需要知道任何有关屏幕、位图、打印机等等之间的不同 -- .NET Framework(以及底层的操作系统)可以为我们处理所有细节。
利用相同的标记,笔和画笔成为虚拟化的绘图工具。笔代表线条属性 -- 颜色、宽度、样式,甚至可以是用来绘制线的位图。画笔代表一个填充区域的属性 -- 颜色、样式,甚至可以是用来填充区域的位图。
在使用 Using 后清除(或至少 Dispose)
Graphics、Pen 和 Brush 对象都与相似类型的 Windows 对象相关联。这些 Windows 对象分配在操作系统的内存中 -- 这些内存尚未被 .NET 运行时管理。长时间将这些对象驻留在内存中会导致性能问题,并且在 Microsoft Windows 98 下,当图形堆填满时会导致绘图问题。因此,我们应尽快释放这些 Windows 对象。
当相应的 .NET Framework 对象完成操作并回收内存后,.NET 运行时会自动释放 Windows 对象。但回收内存的时间会很长 -- 如果我们不迅速释放这些对象,所有不幸的事情(包括填满 Windows 堆)都可能发生。在该应用程序的 ASP.NET 版本中,由于很多用户在同一台服务器上访问该应用程序,所以这种现象会更加严重。
因为这些对象与未管理的资源相关联,所以它们实现 IDisposable 接口。该接口有一个方法,即 Dispose,它将 .NET Framework 对象从 Windows 对象中分离出来,并释放 Windows 对象,从而使计算机处于良好的状态。
这时您只需完成一项任务:确保在使用完该对象后,调用 Dispose 方法。
Visual Basic .NET 代码中显示了这一内容的经典形式:首先创建对象,然后在一个 Try 块中使用该对象,最后在 Finally 块中清理该对象。Try/Finally 能够确保即使出现异常也会清理对象。(在本例中,我们调用的绘图方法可能不会引发异常,所以可能并不需要 Try/Finally 块。但掌握这些技巧很有用,因此 Dr. GUI 也希望向您演示这一正确方法。)
这种形式很常见,因此 C# 为其提供了一个私有语句:using。C# 代码中的 using 语句等同于 Visual Basic .NET 代码中的声明和 Try/Finally -- 但更为简洁、方便,并减少了发生错误的可能性。(Dr. GUI 不清楚为什么 Visual Basic .NET 不包含一些诸如 using 的语句。)
接口
有些(但不是全部)可绘制对象可以被填充。某些对象(如点和线)不能被填充,因为它们不是封闭的区域,而矩形和圆等对象可以是中空的,或者被填充。
另一方面,就象将所有可绘制对象作为多态处理一样,我们也可以将所有可填充对象作为多态处理,这会很方便。例如,如同我们将所有可绘制对象放到一个集合中,通过遍历集合并在每个对象上调用 Draw 来绘制对象一样,我们可以将所有可填充对象放到一个集合中,而不考虑这些可填充对象的实际类型。因此,我们使用某种机制(如继承)来获得真正的多态。
因为不是所有的可绘制对象都可以被填充,因此不能将 Fill 方法的声明放在抽象基类中。.NET Framework 不允许类的多重继承,所以也不能将其放在另一个抽象基类中。并且如果我们从不是非可填充类的其他基类中派生出可填充对象,则不能将所有可绘制对象作为多态处理。
但 .NET Framework 支持接口 -- 并提供了一个可实现任意数量的接口的类。接口不具有任何实现 -- 没有代码,也没有任何数据。因此,实现接口的类必须提供所有内容。
接口所能包含的只有声明。以下是我们在 C# 中的接口 IFillable。单击此处在新窗口中查看全部源文件。
C#
public interface IFillable {
void Fill(Graphics g);
Color FillBrushColor { get; set; }
}
以下是等同的 Visual Basic .NET 代码。单击此处在新窗口中查看全部源文件。
Visual Basic .NET
Public Interface IFillable
Sub Fill(ByVal g As Graphics)
Property FillBrushColor() As Color
End Interface
我们不需要声明方法或者 virtual/Overridable 或 abstract/MustOverride 属性以及任何其他项,因为接口中的所有方法和属性都自动设置为公开的和 abstract/MustOverride。
使用一个属性:不能在接口中包含数据
请注意,虽然我们不能在接口中声明字段,但可以声明一个属性,因为属性实际上是作为方法实现的。
但这样做会给接口的实现者带来负担,下面就会看到。实现者必须实现 get 和 set 方法,以及实现该属性所必需的任何数据。如果实现非常复杂,则可以编写一个 helper 类以封装某些部分。在本文稍后我们将就一个略微不同的上下文环境显示如何使用 helper 类。
实现接口
我们已经定义了接口,现在可以在类中实现它了。请注意,我们必须提供所实现接口的完整实现:不能只从中选取一部分。
下面我们看看 C# 中 DFilledRectangle 类的代码。
C#
public class DFilledCircle : DHollowCircle, IFillable
{
public DFilledCircle(Point center, int radius, Color penColor,
Color brushColor) : base(center, radius, penColor) {
this.brushColor = brushColor;
}
public void Fill(Graphics g) {
using (Brush