结合示例说明类模块与多态

多态

多态意味着许多类可以提供同样的属性或者方法,而且调用者在调用这些属性或方法之前,不必知道某个对象属于什么类。

例如,Flea类跟 Tyrannosaur类可能都有 Bite方法。多态意味着可以调用 Bite方法,而不必知道某个对象是 Flea还是个 Tyrannosaur —尽管以后当然会搞清这一点的。

下面的主题是围绕着 Visual Basic实现多态的途径,以及怎样在程序中使用它而展开的。

·        Visual Basic是如何提供多态的   大多数面向对象的语言,都是通过继承来提供多态的;而 Visual Basic则是用部件对象模型 (COM) 的多接口方法。

·        创建和实现接口   用一个扩展的代码示例,显示了怎样为 Tyrannosaur Flea 类来创建抽象的 Animal接口,并实现它。

·        实现属性   所实现的接口,除了具有方法以外,也具有属性,尽管在实现属性的途径方面有一些差异。

·        关于对象和接口的简要补充讨论   把术语对象和接口搞清,为接口引入了查询的概念,并对其它接口源的实现作了描述。

·        代码重用的许多(内部)方面   除了实现抽象的接口外,也可以通过实现一个普通类的接口来重用代码,然后选择性地委派类的一个隐藏实例为代表。

详细信息   对于 Visual Basic的专业版和企业版,多态已经变成了发展软件部件系统的一种强大的机制。这在《部件工具指南》中创建 ActiveX部件中的部件设计的一般准则中作了讨论。

 

Visual Basic 是如何提供多态的

大多数面向对象的程序设计系统,都是通过继承来提供多态的。也就是说,设想的 Flea Tyrannosaur类可能都是从某个 Animal类继承来的。每个类都将重写 Animal类的 Bite 方法,以提供自己的噬咬特点。

多态来自于这个事实:可以调用属于某类对象的 Bite方法该类可以是从 Animal中派生出来的任意类,而不必知道该对象到底属于哪一个类。

 

提供多态接口

Visual Basic不用继承来提供多态。Visual Basic是通过多重 ActiveX接口来提供多态的。在构成 ActiveX规格说明基础的部件设计模型 (COM)中,多重接口允许软件部件系统在不扩散现有代码的情况下进行展开。

一个接口是一组相关的属性和方法。ActiveX规格说明的许多内容是关于实现一些标准接口的,这些标准接口是用来获得系统服务或者为其它程序提供功能。

Visual Basic中,可以创建一个 Animal接口,并用自己的 Flea Tyrannosaur类予以实现。然后就可以调用任何一种对象的 Bite方法,而无须知道它到底是哪一种对象。

Visual Basic不用继承来提供多态。Visual Basic是通过多重 ActiveX接口来提供多态的。在构成 ActiveX规格说明基础的部件设计模型 (COM)中,多重接口允许软件部件系统在不扩散现有代码的情况下进行展开。

一个接口是一组相关的属性和方法。ActiveX规格说明的许多内容是关于实现一些标准接口的,这些标准接口是用来获得系统服务或者为其它程序提供功能。

Visual Basic中,可以创建一个 Animal接口,并用自己的 Flea Tyrannosaur类予以实现。然后就可以调用任何一种对象的 Bite方法,而无须知道它到底是哪一种对象。

 

多态和性能

 

从性能方面考虑,多态是很重要的。为了认识这一点,考虑下面的函数:

Public Sub GetFood(ByVal Critter As Object, _
ByVal Food As Object)
   Dim dblDistance As Double
   ' 计算到食物所在处距离的代码(略)。
   Critter.Move dblDistance   ' 后期绑定
   Critter.Bite Food            ' 后期绑定
End Sub

Critter来说,Move方法和 Bite方法是后期绑定的。后期绑定在 Visual Basic编译时不能决定变量包含何种对象时发生。在本示例中,Critter参数被声明为 As Object,因此在运行时,它可能包含对任何类型对象引用,如 Car或者 Rock

因为它不可能指明对象将是什么,所以 Visual Basic编译一些附加的代码,用这些代码来询问该对象是否支持已经调用的方法。如果该对象支持这种方法的话,那么附加的代码将调用它;反之,附加的代码将会产生一个错误。每种方法或者属性的调用都会引入这个额外开销。

相比较而言,接口则允许前期绑定。当 Visual Basic在编译时明确知道正在调用什么样的接口时,它将检查一下类型库,看看那个接口是否支持该方法。然后 Visual Basic就可以用一张虚拟函数表 (vtable),按直接跳转到该方法进行编译。这样做比起后期绑定来要快许多倍。

现在,假设 Move Bite方法属于 Animal 接口,而且动物的所有类都提供该接口。Critter参数现在就可以被声明为 Animal了,而且 Move Bite 方法也将是前期绑定的:

Public Sub GetFood(ByVal Critter As Animal, _
ByVal Food As Object)
   Dim dblDistance As Double
   '计算到食物所在处距离的代码(略)。
   Critter.Move dblDistance   '前期绑定 (vtable)
   Critter.Bite Food            '前期绑定 (vtable)
End Sub

详细信息   在下面创建和实现接口这一节中,创建了一个 Animal接口,并在 Flea Tyrannosaur 类中实现它。

 

创建和实现接口

 

如上面“Visual Basic是如何提供多态的中所解释的,所谓接口,就是一组属性和方法。在下面的代码示例中,将创建一个 Animal接口,并在两个类— Flea Tyrannosaur 中实现它。

创建该 Animal接口的方法是:将一个类模块添加到工程中,将它命名为 Animal,并插入到下面的代码中:

Public Sub Move(ByVal Distance As Double)
 
End Sub
 
Public Sub Bite(ByVal What As Object)
 
End Sub

注意,在这些方法中并没有任何代码。Animal是一个抽象类,不包含实现的任何代码。抽象类不是用来创建对象的其用途是为添加其它类中的接口提供模板。(尽管,已经证明,有些时候实现一个非抽象类的接口是有用的;这将在本节主题的后面进行讨论。)

注意   确切地说,一个抽象的类是不能从中创建对象的类。从 Visual Basic的类中总是可以创建对象的,即使它们不含有代码,它们也不是真正抽象的。

现在,就可以添加另外两个类模块了,这两个类模块一个叫 Flea,另一个叫 Tyrannosaur。为了在 Flea类中实现 Animal接口,要用到 Implements语句:

Option Explicit
Implements Animal

一旦将这行代码添加进去,就可以单击代码窗口中左边(对象)下拉菜单。其中一个登录项将是 Animal。当选择了它时,右边(过程)下拉菜单上将显示 Animal 接口的方法。

依次选择每种方法,可为所有的方法创建空白过程模板。这些模板将具有正确的参数和数据类型,就象在 Animal类中所定义的那样。每个过程名都将以Animal_前缀来标识该接口。

重点   一个接口就象一个契约。实现接口后,当调用该接口的任何属性或者方法时,一个类已经约定好要作出反应。因此,必须实现接口的所有属性和方法。

现在,就可以将下面的代码添加到 Flea类中了:

Private Sub Animal_Move(ByVal Distance As Double)
   '(跳过多少英寸的代码,略。)
   Debug.Print "Flea moved"
End Sub
 
Private Sub Animal_Bite(ByVal What As Object)
   '(吃了多少生命的代码,略。)
   Debug.Print "Flea bit a " & TypeName(What)
End Sub

可能想知道为什么这些过程被声明为 Private。如果它们是 Public,那么 Animal_Jump Animal_Bite过程将成为 Flea 界面的一部分,这样将被困在跟原先所在的同样的圈子中,将 Critter参数声明为 As Object,这样它就可能包含一个 Flea或者一个 Tyrannosaur

 

多重接口

 

现在,Flea类有了两个接口:刚刚实现的 Animal 接口它有两个成员,以及缺省的 Flea接口它没有任何成员。该示例的后面,将把一个成员添加到缺省接口的其中一个。

类似地,可以为 Tyrannosaur类实现 Animal 接口:

Option Explicit
Implements Animal
 
Private Sub Animal_Move(ByVal Distance As Double)
   '(跳起多少码的代码,略。)
   Debug.Print "Tyrannosaur moved"
End Sub
 
Private Sub Animal_Bite(ByVal What As Object)
   ' (拿起一磅肉的代码,略。)
   Debug.Print "Tyrannosaur bit a " & TypeName(What)
End Sub

练习 Tyrannosaur和 Flea

将下列代码添加到“Form1” Load事件中:

Private Sub Form_Load()
   Dim fl As Flea
   Dim ty As Tyrannosaur
   Dim anim As Animal
 
   Set fl = New Flea
   Set ty = New Tyrannosaur
   '首先看一下 Flea
   Set anim = fl
   Call anim.Bite(ty)   'Flea 叮咬 dinosaur
   '现在轮到 Tyrannosaur
   Set anim = ty
   Call anim.Bite(fl)   'Dinosaur  flea
End Sub

F8键,单步执行代码。注意立即窗口中的信息。当 变量anim包含对 Flea 的引用时,就调用该 Flea Bite 实现,同样的,对于 Tyrannosaur 来说,情况也是如此。

变量 anim可以包含对实现 Animal 接口的任何对象的引用。事实上,它只能包含对这种类型对象的引用。如果试图将一个 Form或者一个 PictureBox 对象赋给 anim的话,那么将会产生错误。

当通过 anim来调用 Bite 方法时,该方法是前期绑定的,因为 Visual Basic在编译时知道,不论将什么对象赋给 anim,该对象都将有一个 Bite方法。

 

绑定的的问题

 

 

回忆一下“Visual Basic 如何提供多态?”("How Visual Basic Provides polymorphism?")中的 GetFood 过程。可以将 GetFood 过程的 2 版本演示多态的版本添加到“Form1”中,并用下面的代码来取代 Load 事件中的代码:

Private Sub Form_Load()
   Dim fl As Flea
   Dim ty As Tyrannosaur
 
   Set fl = New Flea
   Set ty = New Tyrannosaur
   'Flea 在恐龙上吃饭。
   Call GetFood(fl, ty)
   '相反的情况。
   Call GetFood(ty, fl)
End Sub

单步执行上述代码,显示作为传递给另一接口类型参数的对象引用,是怎样转换为对第二个接口(在这里是 Animal)的引用的。所发生的情况是: Visual Basic 查询该对象,看看该对象是否支持第二个接口。如果该对象支持的话,它将返回对该接口的引用,于是 Visual Basic 将返回的引用放置到参数变量中。如果该对象不支持第二个接口,将会出现错误。

 

实现返回值的方法

 

假设 Move 方法返回一个值。不管怎么说,自己知道想让 Animal 移动多远的距离,但是,各个实例不可能都移动那么远。它可能又老又弱,或者在路上可能恰好有一堵墙。可以用 Move 方法的返回值来说明该 Animal 实际移动了多远的距离。

Public Function Move(ByVal Distance As Double) _
As Double
 
End Function

Tyrannosaur 类中实现该方法时,把返回值赋给过程名,这跟处理任何其它 Function 过程一样:

Private Function Animal_Move(ByVal Distance _
As Double) As Double
   Dim dblDistanceMoved As Double
   '计算能弹跳多远(基于对年龄、健康状态和障碍物的
   '考虑)的代码,略。
   '该示例假设已经将结果放置到变量 dblDistanceMoved 中。
   Debug.Print "Tyrannosaur moved"; dblDistanceMoved
   Animal_Move = dblDistanceMoved
End Function

为了返回值的赋值,使用全过程名,包括接口前缀。

详细信息   所实现的接口,除方法外还可以有属性。下面实现属性讨论了属性实现的方式上的一些不同之处。

 

实现属性

 

本主题继续讨论创建和实现接口开始的代码示例,将属性添加到用 Flea Tyrannosaur 类所实现的 Animal 接口中。当然,在开始该主题前,阅读一下那个主题是有帮助的。

假设要将 Age 属性赋予 Animal 通过将一个 Public 变量添加到声明部分:

Option Explicit
Public Age As Double

现在,在 Tyrannosaur Flea 类的代码模块的过程下拉菜单上,将包含实现 Age 属性的属性过程,如下图 9.10 所示。

9.10   属性过程的实现

这里图示了本章前面的向类中添加属性中的一个要点。严格说来,用公有变量实现属性对于编程者来说是很方便的。此含义的背后,Visual Basic 是用一对属性过程来实现该属性。

必须实现两个过程。通过在私有数据成员中保存值,实现属性很容易,如下所述:

Private mdblAge As Double
 
Private Property Get Animal_Age() As Double
   Animal_Age = mdblAge
End Property
 
Private Property Let Animal_Age(ByVal RHS As Double)
   mdblAge = RHS
End Property

私有数据成员是一个实现细节,必须自己添加。

注意    Implements Property Set 或者 Property Let 提供模板时,并没有任何办法来确定最后一个参数的名字,因此代之以名字 RHS,如上述代码示例所述。

在作为公有数据成员实现的属性上,没有对数据的有效性进行验证,但是那并不意味着不能将 Animal_Age 有效性验证的代码添加到 Property Let 中。例如,可能想分别为 Tyrannosaur 或者 Flea 限制其值为合适的年龄。

事实上,这意味着接口和实现的独立性。只要接口跟类型库中的描述相匹配,实现可以是任意的。

在进行下一步之前,从两个类模块中删除读写 Age 属性的实现部分。

实现只读属性 

 

当然,允许任意设定一个动物的年龄,是一种糟糕的对象设计。对象应该知道自己的年龄,并将这个年龄作为一种只读属性提供给用户。从 Animal 类中删除公有变量 Age,并为只读年龄属性添加模板,如:

Public Property Get Age() As Double
 
End Property

现在,Tyrannosaur 和 Flea 类代码窗口的“过程”下拉菜单上,仅包含一个登录项,即 Age [PropertyGet]。对 Tyrannosaur 可这样实现:

Private mdblBirth As Double
 
Private Property Get Animal_Age() As Double
   Animal_Age = Now - mdblBirth
End Property

上述代码返回 Tyrannosaur 按日计算的年龄。可以在 Tyrannosaur 类的 Initialize 事件中设置 mdblBirth,如:

Private Sub Class_Initialize()
   mdblBirth = Now
End Sub

当然,也可以用更常用的单位来返回属性值,比如犬类的寿命。

详细信息   已经正在到处引申接口和对象的概念,把它们看作是同样的东西,而表面看来则是把对对象的引用放置到对象变量中,把对接口的引用放置到另一个对象变量中。“关于对象和接口的简要补充讨论”澄清许多问题。

关于对象和接口的简要补充讨论

本主题,将完成从“创建和实现接口”开始,并在“实现属性”中继续讨论的那个代码示例。当然,在开始这个主题之前,阅读一下那些主题是有帮助的。

这个 Tyrannosaur 和 Flea 代码示例,看起来似乎用接口和对象来解决很快,也很轻松。把对象的引用赋给一个对象变量,把接口的引用赋给另一个对象变量。

事实上,所有的引用都是对象的引用。对一个接口的引用也是对实现接口的对象的引用。而且,一个对象可能有多个接口,但在底层它仍然是同一对象。

在 Visual Basic 中,每个类都有一个缺省的接口,该接口具有与类相同的名字。是的,几乎都是同样的。按照惯例,在类名前加一条下划线。该下划线指明了这个接口是隐藏在类型库里的。

这样,Tyrannosaur 类就有一个缺省名为 _Tyrannosaur 的接口。因为 Tyrannosaur 同时实现 Animal,所以该类还有第二个接口(其名为 Animal)。

不过,在它的所有底层,对象仍然是 Tyrannosaur。将一个命令按钮放置到“Form1”上,并添加下面的代码:

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal
 
   Set ty = New Tyrannosaur
   Set anim = ty
   MsgBox TypeName(anim)
End Sub

可能期望信息框显示 "Animal",但是,事实上显示的是 "Tyrannosaur"。

查询接口

当把 Tyrannosaur 对象赋给 Animal 类型的变量时,Visual Basic 将询问该 Tyrannosaur 对象它是否支持 Animal 接口。(为此所用的方法为 QueryInterface,或者简称 QI;有时可能遇到 QI 作为动词使用。)如果回答是否定的,将会产生错误。

如果回答是肯定的,该对象将被赋给变量。只有通过这个变量 Animal 接口的方法和属性才可被访问。

类属对象变量和接口

如果将对象引用赋给类属对象变量,(如下列代码所示),将会出现什么情况?

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal
   Dim obj As Object
 
   Set ty = New Tyrannosaur
   Set anim = ty
   Set obj = anim
   MsgBox TypeName(obj)
End Sub

结果得到另一个 Tyrannosaur。现在,通过变量 obj 调用属性和方法时,得到什么接口?将下面的方法添加到 Tyrannosaur 类中:

Public Sub Growl()
   Debug.Print "Rrrrrr"
End Sub

Growl 方法属于 Tyrannosaur 对象的缺省接口。在命令按钮的 Click 事件的代码中,用下面的两行代码来取代 MsgBox 语句:

obj.Move 42
obj.Growl

当运行这个工程并单击按钮时,执行将在 Growl 方法上停止,并返回错误信息“对象不支持此属性或方法”。很明显,接口仍然是 Animal。

Object 类型的变量和具有多重接口的对象一起使用时,必须记住某些事情。该变量将访问的接口是最后赋值的接口。例如:

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal
   Dim obj As Object
 
   Set ty = New Tyrannosaur
   Set anim = ty
   Set obj = anim
   obj.Move 42      '成功
   obj.Growl      '失败
 
   Set obj = ty
   obj.Move 42      '失败
   obj.Growl      '成功
End Sub

幸好,很少将这种比较慢的、后期绑定的 Object 数据类型和具有多重接口的对象一起使用。使用多重接口的一个主要原因是,通过多态可以得到前期绑定的好处。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值