| 创建自己的重载运算符 | |
| 一些重载警告 |
这个月,我在邮箱中发现了一个有趣的问题。该问题的来龙去脉如下:“我正在编写一些使用 Visual Basic® 2003 中的点和其他绘图对象的代码,并且我只是想向点中添加偏移量,以便有效地移动该点。我尝试向 Point 添加 Size,但是代码却无法编译。我在 C# 中尝试了完全相同的事情,并且一切正常。倒底发生了什么事情?”
要设置上下文,应该在 Visual Studio® .NET 2003 中创建一些代码以查看其行为,然后再尝试解决方案(如果在 Visual Studio 2005 中尝试该解决方案,事情将稍有不同)。打开 Visual Basic .NET 2003,创建一个新的 Windows® 应用程序,在窗体上放置一个按钮,双击该按钮以进入代码设计器,然后向该按钮的 Click 事件处理程序中添加以下代码:
Dim pt As New Point(0, 0) pt += New Size(10, 10)
它无法编译,是不是?为了追求乐趣,请尝试相同的练习,但改而使用 C# 编写代码:
Point pt = new Point(0, 0); pt += new Size(10, 10);
该代码可以很好地编译,对吗?我可以详细解释该代码为什么在 C# 中有效以及为什么在 Visual Basic .NET 中无效,但是整个事情归结为一个简单的设计问题:即,Visual Basic 2002 和 Visual Basic 2003 不支持重载运算符。这些语言既不能创建也不能使用已经被重载的运算符 — 即,那些看起来像标准运算符、但是具有由该运算符的操作数之一定义的非标准行为的运算符。
如果您对此略作思考,就会同意,任何运算符(即使是对整数执行运算的普通的 + 和 - 运算符)都只是方法的一种便利书写方式。例如,在将两个整数相加时,不是像下面这样编写代码
Dim x As Integer = a + b
您可以想像编译器在背后生成了如下所示的代码:
Dim x As Integer = Int32.Add(a, b)
只要运算符是由编译器定义的,Visual Basic 2002 和 Visual Basic 2003 就能够使用它正常工作。例如,您是否考虑过对字符串使用 + 运算符?尽管大多数开发人员不会编写如下代码,但这些代码却是完全可以接受的,并且能够正常工作:
Dim str1 As String = "Hello, " Dim str2 As String = "there" Dim str3 As String = str1 + str2
在背后,字符串的“+”运算符已经被重载以调用 String.Concat 方法,从而将最后一行代码转换为如下所示的代码:
Dim str3 As String = String.Concat(str1, str2)
我猜想,大多数人从来没有像这样考虑过问题,因为这一切都是由编译器以透明方式执行的,并且它使得对所涉及的类的使用变得更加直观。
现在回到原来的问题。因为 C# 从一开始就支持运算符重载,同时还因为在 Point 结构的代码中的某个地方隐藏着一个重载 + 运算符并且接受 Point 和 Size 作为参数的过程,所以 C# 开发人员可以使用 + 运算符将 Point 结构和 Size 结构加在一起,从而在新位置产生一个新的 Point。那么,如果使用 Visual Basic 的开发人员希望将 Point 和 Size 加在一起,就会完全失败吗?并非完全如此。
当您在 C# 代码中对 Point 结构使用 + 运算符时,C# 编译器随后会生成对该结构上的一个执行实际工作的 Shared 方法的调用。在这种情况下,该方法名为 op_Addition,并且正如您可能期望的那样,从 Visual Basic .NET 代码中像调用其他任何方法一样显式地调用它。要对此进行测试,请返回无法编译的 Visual Basic .NET 项目,并修改代码以使其如下所示:
Dim pt As New Point(0, 0) pt = Point.op_Addition(pt, New Size(10, 10))
任何人都会承认,该版本要比原来的版本难看一些,但至少该代码能够正常工作。您如何才能找到该过程呢?请参阅有关 Point 结构的文档。该文档本身并没有什么帮助,但是如果您单击“Members”页上的“Addition Operator”链接,则可以找到使您似乎能够使用相加、相等和其他运算符的信息。尽管您无法按照与其他一些语言中相同的方式来使用实际的运算符符号,但您可以显式地调用这些运算符函数。给定这一额外信息,在使用 Visual Basic 时,尽管开发人员无法直接使用运算符符号,他们也可以调用方法。
如果您多花一些时间来阅读该文档,则会发现还有与其他运算符和转换相对应的方法,如图 1 所示。为什么这两个转换列在图 1中,并且被视为运算符呢?让我们回过头去研究一下转换是如何工作的。转换在 .NET 中的处理方式非常类似于运算符,因此,如果您要键入以下代码,则无论您知道与否,您都将使用扩大和收缩转换运算符 — 它们都已内置到 Integer 类的定义中:
Dim x As Integer = 12 Dim y As Long = x Dim z As Short = x
如果您已经启用了 Option Strict(我的确希望您这样做),则最后一行无法编译。Visual Basic .NET 编译器不会将 Integer 转换为 Short,因为在执行转换(从四个字节的值转换为两个字节的值)时,您可能会丢失信息。从 Integer 到 Long 的转换是扩大转换 — 不会丢失数据。另一方面,从 Integer 到 Short 的转换是收缩转换 — 在这样的转换中,有可能会丢失数据。在启用 Option Strict 以后,Visual Basic 不允许执行与此类似的隐式收缩转换,因而上述代码无法编译。
有趣的是,该示例代码只使用一个运算符。赋值运算符“=”可处理扩大和收缩这两种转换。在 C# 中,可以将赋值运算符用于图 1中列出的两种 Point 转换操作。但是,在这两种情况下,代码并不完全相同。执行从 Point 到 PointF 的扩大转换会隐式地从一个类型转换到另一个类型,但是执行从 Point 到 Size 的收缩转换则要求使用显式转换,如图 2 所示。
但是,如果没有调用已公开的方法,则上述两种机制都无法在 Visual Basic 2002 或 Visual Basic 2003 中正常工作。如果我只讨论上述两个版本,那么现在就可以结束了。
随着 Visual Basic 2005 的迫近,在运算符重载领域中,事情将会发生变化。Visual Basic 2005 添加了对重载运算符的支持,从而使您可以使用和创建重载运算符。尽管您不太可能每天都执行该工作,但知道您可以这样做总是有好处的。
如果您使用 Visual Basic 2005 尝试我的第一个代码片段,则您将注意到,即使启用了 Option Strict,编译器也不会抱怨。请尝试以下代码(它类似于前面的 C# 片段),您将看到该代码也可以正常工作:
' Just as in C#, a widening conversion can be implicit: Dim pt1 As New Point(0, 0) Dim ptf As PointF = pt1 ' Just as in C#, a narrowing conversion must be explicit: Dim pt2 As New Point(0, 0) Dim sz As Size = CType(pt2, Size)
在我已经见过的几乎每种课本中,典型的运算符重载示例都是复数演示。通过阅读 Matthew Gertz 撰写的一篇题目为“Operator Overloading in Visual Basic 2005”的出色文章,您可以找到使用该示例对运算符重载进行的清楚易懂的说明。但是,对于本专栏,我将在一个称为 Vector 的类中使用一个 Integer 数组。这是一个相对容易理解的简单示例,坦率地说,在您编写的很多应用程序中,您都不太可能需要复数。您有能力重载图 3 中列出的任何运算符。
通常,您应当注意下列有关运算符重载的重要事实:
| • | 只有图 3中列出的运算符可以重载。无法重载成员访问 (.)、方法调用或 AndAlso、OrElse、New、TypeOf…Is、IsNot、AddressOf 和 GetType 中的任何一个。使用 = 符号作为赋值运算符是不可重载的。 |
| • | 尽管您通常可以重载任何单个运算符,但某些运算符(具体说来,就是逻辑运算符)是成对工作的。如果重载 =,则必须重载 <>。如果重载 <,则必须重载 >,等等。 |
| • | 运算符重载过程必须与该运算符的其中一个操作数位于相同的类中,并同时使用 Public 和 Shared 关键字声明。 |
| • | CType 的重载必须表明它们是扩大转换还是收缩转换。扩大的 CType 重载定义了赋值运算符的重载。(即,在执行收缩转换时,您必须使用 CType,但是在执行扩大转换时,您可以直接使用赋值。该技术使您可以有效地重载赋值运算符。) |
为了理解这一说明,请在 Visual Basic 2005 中创建一个新的 Windows 应用程序,并向名为 Vector 的项目中添加一个新类。在该类中,添加图 4 中所示的代码。该类包含一个简单的整数数组,并且您必须在该类的构造函数中传递您喜欢的数组大小。
在项目内部的 Form1 上放置一个按钮,并且向该按钮的 Click 事件处理程序中添加图 5 中所示的代码。该代码会创建新类的两个实例。
给定上述的 Vector 类,您可能希望插入使您可以向标准的 + 运算符中添加一个向量的功能(显然,您还可以将该概念扩展到其他操作)。您可能还希望能够使用 = 运算符比较两个向量,并确定它们的内容是否完全相同。最后,您可能希望能够使用 CType 运算符从 Integer 值的一维数组转换到 Vector,或者从 Vector 重新转换回数组。运算符重载使上述所有操作都成为可能。
要添加对于添加两个 Vector 实例的支持,可以向 Vector 类中添加如图 6 中所示的代码。
给定 + 运算符的上述所有重载版本,您可以使用如下所示的代码来测试 Vector 类:
Dim v3 As Vector = v1 + v2 ' 0, 2, 6, 12, 20 MessageBox.Show(v3.ToString())
如果您希望重载等于 (=) 运算符,以便可以比较两个 Vector 实例,则可以向 Vector 类中添加如图 7 的上半部分中所示的代码。请注意,如果您重载 =,则还必须重载 <>。
为了能够转换到整数数组以及进行反向转换,请向 Vector 类中添加如图 7的下半部分中所示的重载。要测试转换运算符,请向窗体类中按钮的 Click 事件处理程序中添加以下代码:
Dim intValues1() As Integer = {1, 2, 3, 4, 5, 6, 7, 8}
' Note that narrowing conversion must be explicit.
Dim v5 As Vector = CType(intValues1, Vector)
MessageBox.Show(v5.ToString())
Dim intValues2() As Integer
' Note that widening conversion can be explicit.
' After this, intValues2 will contain the same
' data as v5.
intValues2 = v5
您现在已经创建了一个 Vector 类,并且该类重载了它的 +、= 和 CType 运算符。我本来可以从我以前的专栏(请参阅 Advanced Basics:Being Generic Ain't So Bad)中采纳我自己的建议,并使用一般的数组而不是 Integer 数组,但是我不希望混淆这里要讨论的问题。
为了防止您认为运算符重载是一种每天都要使用的技巧,我希望向您澄清一个事实:您将很少重载运算符。当您确实要重载运算符时,它们最好能够使您的类变得更直观和更容易使用,而不是仅仅使您看起来很聪明。如果在您使用重载运算符执行操作时,所发生的事情并不是一目了然,那么请将其放弃。例如,假设有一个用于跟踪客户、订单和订单明细信息的订单输入系统。因为您通常希望显示某个客户的所有订单数量的总和,所以您需要一种相应的方法,以便合计该客户所有订单中的 Order 类的 Total 字段。您可以重载 + 运算符以便合计订单:
Dim total As Long = 0 For Each ord As Order In Customer1.Orders ' This is really the same as ' total = total + ord total += ord Next
为了使该示例能够工作,您可以重载 + 运算符以接受 Long 和 Order 作为操作数,并且返回 Long 作为结果。但是,该代码不见得比采用冗长方式编写的代码更为清晰,如下所示:
Dim total As Long = 0 For Each ord As Order In Customer1.Orders total += ord.Total Next
我确实不认为您获得了任何效率或清晰度 — 如果您得到了什么东西的话,那也只能是代码的含义变得模糊了。在任何情况下,都要审慎地使用运算符重载 — 但大多数情况下都不需要使用它。
不要认为仅仅因为某些开发人员用 C# 编程,他们就无法利用您的重载 — 这些功能可以跨语言工作,并且您在 Visual Basic 2005 中创建的任何重载运算符也都应当能够在 C# 中很好地工作。(反之亦然,因为 Visual Basic 2005 可以使用重载运算符。)
在开始使用运算符重载之前一定要三思而行,但当您需要使用这一很好的技术时,请不要有什么顾虑。当然,在 Visual Basic 2005 问世之前,您将必须平心静气地调用现有的“op_Addition”方法,但只要 Microsoft 发布了 Visual Basic 2005,一切就会变得轻而易举。
Ken Getz 是 MCW Technologies 的高级顾问。他与人合著了 ASP .NET Developers Jumpstart (Addison-Wesley, 2002)、Access Developer's Handbook (Sybex, 2001) 和 VBA Developer's Handbook, 2nd Edition (Sybex, 2001)。可以通过 keng@mcwtech.com 与他联系。