继承与接口
升级到微软 .NET
Andy Baron
MCW 技术公司
2002年2月
概述:了解在Microsoft Visual Basic .NET中的类继承与接口实现的区别。
目标
研究继承与接口使用背后的概念
学习何时使用类继承,何时使用接口实现
要求
为了充分利用本文,读者必须具备以下基础:
熟悉Microsoft® Visual Basic® .NET语言
熟悉Microsoft Visual Basic 6.0
基本理解继承中的术语
目录
面向对象编程:为什么编程?
继承层次
创建和实现接口
比较类继承与接口实现
对象合成
与 Visual Basic 6.0的区别?
小结
面向对象编程:为什么麻烦?
只有一小部分Visual Basic 6.0程序员发现他们在构建Visual Basic 6.0窗体时需要创建类模块,而不是使用自动创建的窗体。另外,大多数的确在Visual Basic 6.0中创建过类模块的程序员,创建创建类模块的原因,只是因为要构建ActiveX®控件,并且不得不使用类模块才能做到。
在Visual Basic .NET中,就象在所有其它语言中一样,面向对象编程(OOP)不是一个选择,而是一种需求。每段代码都是某种类型,如类、接口、结构(结构是值类型,类似于Visual Basic 6.0中的用户自定义类型),或枚举值的一部分。甚至在Visual Basic .NET中看上去独立的过程实际上也是作为隐藏类的共享方法实现的。
为何OOP如此强大?为什么微软一直要求大家学习用OOP在Visual Basic .NET中编程?
处理复杂性与变化
OOP解决两个普遍的软件开发问题:处理复杂性和处理变化。利用OOP就容易设计和使用复杂的软件系统,并且易于修改这些系统而无需打乱它们。
Visual Basic 6.0程序员需要用ADO或DAO对象模型获得和操作数据,设想,如果每个操作都必须调用独立的函数,而不是使用对象(如记录集)的方法或属性,完成这样的工作会有多么艰巨。例如,如果要用ADO或DAO向列表框中装入数据,大约要编写10行代码。而直接使用ODBC API函数,大约要编写50行代码!可以看到,OOP方法更容易实现。
再例如,考虑微软Windows® API。如果要花大量时间调用Windows® API或使用其它面向过程的APIs,就会发现,为一个任务要调用哪个函数或过程,是多么容易出错、多么难记。因此,通过创建相关对象,把问题模型化,对程序员来说,要比使用一长列的过程或函数要友好的多。
编写和调试代码是困难的,但大多数程序员喜欢跟踪问题,设计也一种灵活方式解决,然后“玩弄”代码,直到一切都满足了客户需求。这是有趣的人的一部分,但并不是事情的结局。所有程序员都害怕的一件事是:不得不研究其他人编写的代码(或他们在很久以前刚开始编写代码时所写的代码),不得不设计也程序各个部分如何搭配,如何进行修改以满足新需求——所有这些都必须不能引起意想不到的后果。不幸的是,在大多数系统生命周期中,维护代码所消耗程序员的时间,比创建这些代码的时间还要多,这就是这些系统的结局。
通过清楚地分离暴露的公有类属性、方法和事件与类成员的隐藏实现,在对象中封装功能就支持进行修改。只要公有接口受到保护,私有实现就可以安全实现,这就是目标。下面将研究确保这个目标实现的方式。
继承与接口
除了通过在对象的方法和属性中封装功能,使复杂系统易于创建和修改外,OOP还支持类继承和接口实现,通过这种方式,新类就能安全地加入到现有系统中而不必修改现有代码。通常将这两种方法比作准备变化,研究每种方法何时是合适的,并讨论第三种方法——对象组合,此方面经常是最好的选择。
继承层次
在所有存在于Visual Basic .NET中OOP技术中,继承对来自Visual Basic 6.0的程序员来说是最不熟悉的,因为在Visual Basic 6.0中不存在继承。甚至是Visual Basic 6.0程序员乐于使用的非Visual Basic 对象模型,如ADO或Office库,也很少需要理解继承。
通过继承可以创建新类,它是现有(基)类的变体,并且在任何最初调用基类的情况中可以用新继承类替换。例如,有一个名为SalesOrder的类,从这个类中派生一个名为SalesOrder的类:
Public Class WebSalesOrder
Inherits SalesOrder
现在任何使用SalesOrder类型对象的方法,都将保持不变,并且也能处理WebSalesOrder对象。如果SalesOrder有一个Confirm方法,那么就可以确保WebSalesOrder 类也有一个Confirm方法。但如果在SalesOrder类中Confirm方法被标记为可重载的,那么就可以在WebSalesOrder类中创建新的Confirm方法来重载原有方法:
Public Overrides Sub Confirm
可能原始Confirm方法向客户发送传真,而WebSalesOrder 类中的Confirm方法使用电子邮件。重要的是,一个最初被设置为可以接受SalesOrder类型对象的方法,现在如果向它传递WebSalesOrder对象,那么此方法也能正常运行,并且该方法调用对象的Confirm方法后,客户将收到电子邮件而不是传真。旧代码不需要做任何修改就可以调用新代码。
另一方面,假设有一个Total,对两个类来说其运行情况完全相同。派生类WebSalesOrder不需要对这个方法作任何修改——它的实现将自动从基类继承,并且可以调用任何WebSalesOrder对象的Total方法,使此对象的行为就象SalesOrder对象一样。
多态性
多态性,或可替代性,是继承是明显的一个优点:不论何时创建了派生类对象,在使用基类对象的地方都可以使用此派生类对象。WebSalesOrder对象 "is a" SalesOrder对象,WebSalesOrder对象必须能实现SalesOrder对象的所有功能,即使是它以自己独特的方式。
多态性很多OOP优点的关键因素,在本文后面可以看到,它不仅存在于继承中,还存在于接口中。利用多态性,不同类型的对象就可以处理交互时使用的一组通用消息,并且是以它们各自的方式进行。
订单确认代码不需要知道如何执行确认,也不需要知道被确认订单的类型。它只关心它是否能够调用所处理的订单对象的Confirm方法,它要依赖此对象处理确认细节:
Public Sub ProcessOrder(order As SalesOrder)
order.Confirm
在调用ProcessOrder过程时,需要向它传递SalesOrder或WebSalesOrder对象,传递任何一个对象程序都能运行。
虚方法和属性
只有派生类重载基类方法时才用到虚拟,虚拟可以是继承中最具神秘性的概述。其神秘性在于.NET 运行时自动找到并运行被调用方法或属性的最特殊实现。
例如,调用上面示例中的order.Confirm方法将会调用WebSalesOrder类的Confirm方法,此处SalesOrder.Confirm方法被重载了。然而,调用order.Total方法则会调用SalesOrder类的Total方法,因为在WebSalesOrder类中没有创建专有的Total方法。
抽象类和方法
抽象是包含了至少一个必须被重载的方法或属性的类。例如,创建了一个没有实现订单确认的SalesOrder类。因为不同类型的订单必须以不同的方式确认,这样就要从SalesOrder类派生类并重载Confirm方法,提供它自己的实现。
这意味着永远不能创建SalesOrder对象。相反,只能使用派生类创建对象,填充如何执行订单的细节。由于这个原因SalesOrder类将被标记为MustInherit:
Public MustInherit Class SalesOrder
Public MustOverride Sub Confirm()
Public Sub Total()
'计算总数的代码
End Sub
如果必须重载所有类成员,那么这个类就被称为纯抽象类。例如,SalesOrder类可以只包含必须被派生类重载的方法和属性。但只要有一个成员必须被重载,这个类就必须用MustInherit属性标记为抽象类。
类型检查与向下造型
如果有些订单需要确认有些订单不需要确认,结果会怎么样呢?可以创建不包含Confirm方法的SalesOrder基类,而只在需要确认的派生类中增加Confirm方法。但这样会出现一个问题:现在希望创建一个程序处理所有类型的订单而在需要时才进行确认。如何知道哪个订单需要确认呢?
为此程序提供一个SalesOrder类型的参数,以允许所有从SalesOrder派生的类型都能传入。但这样就需要在调用Confirm方法前对所有的确认类型进行测试。而且,一旦发现订单类型是可确认的,那么就需要象下面这样从SalesOrder类(此类中不存在Confirm方法)向下造型到派生类:
Public Sub ProcessOrder(order As SalesOrder)
If TypeOf(order) Is WebSalesOrder Then
CType(order, WebSalesOrder).Confirm
ElseIf TypeOf(order) Is EmailSalesOrder Then
CType(order, EmailSalesOrder).Confirm
' 等等
这种类型的代码很难维护。每次在系统中从SalesOrder派生一个类后,必须修改此方法。旧代码与新代码的这种耦合正是所要避免的。
这就同在此过程中实现不同类别的销售订单的确认代码一样糟糕
If TypeOf(order) Is WebSalesOrder Then
' 在此编写确认 WebSalesOrder的代码
ElseIf TypeOf(order) Is EmailSalesOrder Then
'在此编写确认 EmailSalesOrder的代码
' 等等
这种方式更为糟糕:每次创建新类型时都必须修改此程序,而且每种销售对象也不再自己进行确认,结果使程序员很难添加新类型的订单。程序员如何才能知道,在处理新类型订单时哪些地方需要添加代码呢?
另一种方法是创建名为ConfirmableSalesOrder的中间抽象类型(从SalesOrder类中派生,带有必须重载的方法Confirm)。然后从ConfirmableSalesOrder中派生需要进行确认的类型,其它类型直接从SalesOrder派生。程序必须检查传入的SalesOrder对象是否是ConfirmableSalesOrder类型,如果是,将使用该类型调用Confirm方法。
If TypeOf(order) Is ConfirmableSalesOrder Then
CType(order, ConfirmableSalesOrder).Confirm
要使用Confirm方法,仍需Ctype造型转换。不过,通过虚拟性,调用将自动传递到创建订单对象的类中,并运行在该类中定义的Confirm方法。
问题看上去解决了,但这只是临时解决方案。猜出为什么了吗?假定下一步要处理的事情是:某些类型的订单需要信用卡。需要信用卡的订单有一经过了确认,而另一些没有。出现问题了吧。
.NET中不存在多重继承
可以从SalesOrder中派生CreditCheckableSalesOrder类型。一些订单类型从ConfirmableSalesOrder派生,另一些从CreditCheckableSalesOrder中派生,但需要确认又需要信用卡的订单会怎么样呢?.NET Framework中对继承的一种限制是一个类型只能从一个基类型中派生。
订单类型不能既从ConfirmableOrder派生又从CreditCheckableOrder派生。这可能被认为是专横的或被误导的限制,但这样做有好多原因。在C++中支持多重继承。然而,所有其它流行的面向对象的语言,包括Java,都不允许多重继承。(一些高级语言,如Eiffel,曾试图设计出不同类型的多重继承,用于.NET 的Eiffel甚至在微软.NET平台上提供了多重继承的样子。)
多重继承最大的问题是,当编译器需要找到虚方法的正确实现时,会出现不确定性。例如,设想Hound 和 Puppy都从Dog中派生,而又BabyBasset从Hound 和 Puppy继承:
图 1. 多重继承的问题
假设Dog有一个可重载的Bark方法。Hound重载了它使它听起来象怒号,Puppy也重载了它使它听起来象尖叫,但BabyBasset没有重载Bark。如果创建了BabyBasset对象,然后调用它的方法,结果会怎样呢,怒号还是尖叫?
.NET Framework要求,派生类只能有一个基类,从而阻止了这种问题的发生。这种限制也意味着每个类最终从单个曾祖父辈System.Object类派生。
单路径类继承意味着.NET对象中以被System.Object类型作为参数的方法处理。单路径类继承在碎片收集中是至关重要的,因为碎片收集器要释放被不可访问对象占用的内存,而如果是这种情况,它就能处理所有类型的对象。
允许所有对象都向上造型(upcast)为通用类型,展现其优点的一个更熟悉的例子是事件处理程序:
Public Sub MyEventHandler(By Val sender As _
System.Object, By Val e As System.EventArgs)
这个处理程序可以连接到来自任何对象或对象的任意组合的事件,这是因为参数sender是System.Object类型,而任意.NET对象都能代替它。如果必要,可以使用System.Reflection.GetType()识别sender对象是何种类型。
Creating and Implementing Interfaces创建和实现接口
继承的这种微妙性是非常有趣的,但对于需要确认和/或信用卡的销售订单,应怎样做呢?答案是使用接口。
类的多重继承引发的问题是由继承链中通用方法间的潜在冲突引起的。但是,对于所继承的类是没有具体实现的纯抽象类,情况会如何呢?在这种情况下,多重继承不会引发任何问题,因为没有具体实现,也就不会引起冲突。这就是接口所提供的功能:继承一组方法和属性说明,但不关心其具体实现,因此从多个接口中继承不会引起问题。
虽然经常使用短语“接口继承”,但正确的术语是接口实现。一个接口从另外一个接口继承是可能的,因此能够将接口操纵方法集扩展到包含它所继承的接口的方法。然而,要在Visual Basic .NET类中使用接口,就要实现这些接口而不是继承它们:
Public Interface IConfirmable
Sub Confirm()
End Interface
Public Class WebSalesOrder()
Inherits SalesOrder
Implements IConfirmable
Public Sub Confirm() Implements IConfirmable.Confirm
' 确认web 订单的代码
End Sub
' 另一个WebSalesOrder 代码
End Class
(在C#中,冒号被用于表达类继承和接口实现。这可能就是它通常在接口名前加前缀“I”的原因。通过这种方式,C#程序员就能容易地区分基类和接口。)
可以创建一些销售订单类型,一些实现了IConfirmable,一些实现了ICreditCheckable,而还有一些两者都实现了。要检查订单是否需要确认,所使用的代码与检查订单是否是从特定类型继承的,并将其造型为那种类型的代码是一样的:
Public Sub ProcessOrder(order As SalesOrder)
If TypeOf(order) Is IConfirmable Then
CType(order, IConfirmable).Confirm
接口多态性
接口也提供了派生类所具有的多态性优点。例如,可以将任何实现了Iconfirmable的类对象传递给需要Iconfirmable参数的方法:
Public Sub ConfirmOrder(order As IConfirmable)
order.Confirm
End Sub
如果WebSalesOrder 和 EmailSalesOrder都实现了,那么就可以将任一对象传递给ConfirmOrder方法。当调用order.Confirm时,在适当类中实现的确认代码将运行。即使没有将类的方法命名为Confirm,但只要将它标记为实现了IConfirmable接口的Confirm方法,程序就能正常运行。
Public Class WebSalesOrder()
Inherits SalesOrder
Implements IConfirmable
Public Sub ConfirmWebOrder() _
Implements IConfirmable.Confirm
' 确认Web订单的代码
End Sub
自由命名是一个有用的特性。如果类实现了两个不同的接口,而恰好接口中具有相同名字的方法,就要利用这种特性了。
比较类继承与接口实现
创建派生类与实现接口间最重要的技术差别是,派生类只能从一个基类继承,但一个类可以实现多个接口。
从设计角度讲,继承是表达一种特殊类型的关系。如果WebSalesOrder是一种特殊类型的SalesOrder,那么就要考虑使用派生类了。
然而,要小心,当区分派生类与基类的专有性也是其它类需要支持的特性时,不要使用继承。对于向类增加这种类型的特性或能力时,接口实现提供了更大的灵活性。
继承用于构建框架
设计有用的继承层次需要详细规划,并且清楚如何使用继承。要把它当作有经验的软件设计师(而他正在创建框架,程序员要利用此框架构建众多应用程序)需要完成的任务,而不是当作在简单地构建特定应用程序时所使用的战略。
.NET Framework自身就包含了许多正在被使用的继承实例,并且需要创建派生类才能执行很多通常的编程活动。例如,可以从ApplicationException类派生自己专有的异常类,以存储定制的错误信息。当释放定制事件后,通过从EventArgs派生一个定制类将信息发送到事件处理程序。要创建特定类型的集合,可以从CollectionBase类中派生。每次在Visual Studio .NET中创建Windows窗体时,就从Windows.Forms.Form基类中派生了一个类。
你应当习惯从框架设计师提供的基类中派生类,但在创建自己的基类时要小心。要确保正在表达一个清晰的层次,并且分析出了客户端程序员要重载的行为。
创建自己的接口时也要小心,但使用接口比使用继承更不容易走到死角,因此在可以进行选择的情况下优先考虑它们。
对象组合
当在考虑创建自己的继承层次时,不要过多地被重用代码的呼声所影响。重用自身不是创建派生类的足够原因。
与其使用继承允许新对象使用现有对象的代码,不如利用称为组合,包容,聚合或封装的技术。在Visual Basic 6.0中你可能使用过这种技术,但在Visual Basic 6.0中并不存在继承。
例如,要创建WebSalesOrder类,此类重用了SalesOrder类中的所有代码,并增加了一些新代码,那么就在WebSalesOrder类中声明并创建SalesOrder对象的一个实例。可以公开暴露内部SalesOrder对象,也可将其保存为私有。
代理
如果SalesOrder类中有一个Total方法,通过简单地调用私有SalesOrder实例的Total方法,WebSalesOrder类也就有了Total方法。将方法调用(或属性调用)传递到内部对象的技术通常称为代理,但不要将它与.NET中利用代理对象创建回调函数或事件处理程序的用法混淆。就象在Visual Basic 6.0中一样,通过利用WithEvents关键字声明被包含的对象,被包含对象的事件可以暴露在封装类中。
将接口实现与组合结合起来
使用对象组合与代理的主要缺点是,不能自动获得象派生类那样的多态性。如果WebSalesOrder对象简单地包含了SalesOrder对象,而不是从另一对象中派生,那么就不能将WebSalesOrder对象传递到需要SaleOrder类型作为参数的方法。
通过创建ISalesOrder接口,而WebSalesOrder实现此接口,就可以克服这个缺点。也可以为被包含SalesOrder对象的方法和属性提供代理。或,如果需要,WebSalesOrder也可以独立的实现接口中的方法和属性,而不是代理到SalesOrder对象。这与派生类的重载类似。
将对象组合与代理同接口实现结合起来,就可以重用代码,实现多态性,而无需设计令人头痛的继承。由于这个原因,当需要对类进行扩展或提供专有功能时,优先考虑这种方法。
与Visual Basic 6.0的区别是什么?
在Visual Basic 6.0中,每次创建一个类模块时,都自动创建一个同名的类接口。考虑下面的Visual Basic 6.0代码:
' Visual Basic 6.0 代码
Dim myObject As MyClass
Set myObject = New MyClass
在这段代码中,第一次使用MyClass时,指向了包含空方法和属性的隐藏接口,第二个MyClass是实现了这些方法和属性的具体类。Visual Basic 6.0对你屏蔽了接口的使用,而接口的使用对于底层的COM管道是非常重要的。
Visual Basic 6.0中的Implements关键允许明确使用接口,并利用存在于Visual Basic .NET中基于接口的多态性。然而,类实现必须使用合并了接口名的命名转换:
' Visual Basic 6.0 IConfirmable 类模块
Public Sub Confirm()
End Sub
' VB6 WebSalesOrder 类模块
Implements IConfirmable
Private Function IConfirmable _Confirm()
' 此处实现确认订单的代码
End Function
任何以这种方式实现了IConfirmable 接口的Visual Basic 6.0对象都可传递给需要Iconfirmable类型作为参数的程序,从而在Visual Basic .NET中支持了接口提供的同种类型的多态性:
' Visual Basic 6.0 或 Visual Basic .NET
Public Sub ConfirmOrder(order As IConfirmable)
order.Confirm
End Sub
虽然在Visual Basic .NET中使用接口的语法更复杂、不易混淆,但最大的变化是对继承的支持。从旧类中派生新类,利用旧类的功能在Visual Basic 6.0中是不可能的,因此没有办法重载选择的方法。
支持继承是一个很重要的变化,但这并不是因为迫切需要创建基类,并从中派生新类。它的最大价值是能够继承C#、C++或其它.NET语言程序员所使用的框架类。如果在Visual Basic .NET中创建了基于继承的框架,那么其它.NET程序员就可以使用此框架。这就使Visual Basic .NET同其它.NET位于同一水平,打破了过去隔离Visual Basic 程序员的屏障。
小结
本文学习了类继承与接口实现间的区别。继承支持专有性不断增加的类层次框架,它们共享一些代码,并进行了定制。接口允许多个不相关的类共享可预测的方法和属性。接口和继承提供了多态性,这样一般性的过程就可以使用不同类型的对象。还学习了如何利用对象组合而不是继承重用和扩展实现代码,对象组合如何与接口结合起来支持多态性。所有这些技术都用于创建和修改复杂的软件系统,以增加新功能,而最小化研究以前代码的需要。
关于作者
Andy Baron是MCW技术公司的高级顾问,是佛罗里达州Singer Island的微软认证解决方案供应商。Andy经常在技术刊物上发表文章,并经常出席行业会议。从1995年以来他每年都收到微软的最有价值专家(MVP)奖。他还为Application Developers Training Company (http://www.appdev.com) 公司编写制作了课件。
关于 Informant 通讯集团
Informant通讯集团公司(www.informant.com)是以信息技术领域为核心的多样化媒体公司。它成立于1990年,并专注于软件开发出版物,讨论会,目录发行和网站。在美国和英国设有办事机构,ICG成为一家受欢迎的媒体公司,并销售目录集成器,满足了IT专业人员对于高质量技术信息不断增长的需求。
版权所有 © 2002 Informant通讯集团公司和微软公司
技术编辑:PDSA公司或KNG Consulting.