多态意味着许多类可以提供同样的属性或者方法,而且调用者在调用这些属性或方法之前,不必知道某个对象属于什么类。 例如,Flea 类跟 Tyrannosaur 类可能都有 Bite 方法。多态意味着可以调用 Bite 方法,而不必知道某个对象是 Flea 还是个 Tyrannosaur —尽管以后当然会搞清这一点的。 下面的主题是围绕着 Visual Basic 实现多态的途径,以及怎样在程序中使用它而展开的。 Visual Basic 是如何提供多态的 大多数面向对象的语言,都是通过继承来提供多态的;而 Visual Basic 则是用部件对象模型 (COM) 的多接口方法。创建和实现接口用一个扩展的代码示例,显示了怎样为 Tyrannosaur 和 Flea 类来创建抽象的 Animal 接口,并实现它。 实现属性 所实现的接口,除了具有方法以外,也具有属性,尽管在实现属性的途径方面有一些差异。 关于对象和接口的简要补充讨论把术语对象和接口搞清,为接口引入了查询的概念,并对其它接口源的实现作了描述。 代码重用的许多(内部)方面 除了实现抽象的接口外,也可以通过实现一个普通类的接口来重用代码,然后选择性地委派类的一个隐藏实例为代表。 大多数面向对象的程序设计系统,都是通过继承来提供多态的。也就是说,设想的 Flea 和 Tyrannosaur 类可能都是从某个 Animal 类继承来的。每个类都将重写 Animal 类的 Bite 方法,以提供自己的噬咬特点。 多态来自于这个事实:可以调用属于某类对象的 Bite 方法—该类可以是从 Animal 中派生出来的任意类,而不必知道该对象到底属于哪一个类。 提供多态接口 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 是如何提供多态的(1)”中所解释的,所谓接口,就是一组属性和方法。在下面的代码示例中,将创建一个 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 方法。 将 Tyrannosaurs 和 Fleas 传递给过程 回忆一下“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 为了返回值的赋值,使用全过程名,包括接口前缀。
却不能真正达到继承能够重用代码的真正功效。
不过已经能够解决类的叠代问题,提高了类应用的灵活度,也许这就是你所说的多态需要体会其中的奥妙,应该看看MSDN提供的例子
例如,Flea 类跟 Tyrannosaur 类可能都有 Bite 方法。多态意味着可以调用 Bite 方法,而不必知道某个对象是 Flea 还是个 Tyrannosaur —尽管以后当然会搞清这一点的。
下面的主题是围绕着 Visual Basic 实现多态的途径,以及怎样在程序中使用它而展开的。
Visual Basic 是如何提供多态的 大多数面向对象的语言,都是通过继承来提供多态的;而 Visual Basic 则是用部件对象模型 (COM) 的多接口方法。创建和实现接口用一个扩展的代码示例,显示了怎样为 Tyrannosaur 和 Flea 类来创建抽象的 Animal 接口,并实现它。 实现属性
所实现的接口,除了具有方法以外,也具有属性,尽管在实现属性的途径方面有一些差异。 关于对象和接口的简要补充讨论把术语对象和接口搞清,为接口引入了查询的概念,并对其它接口源的实现作了描述。 代码重用的许多(内部)方面 除了实现抽象的接口外,也可以通过实现一个普通类的接口来重用代码,然后选择性地委派类的一个隐藏实例为代表。 大多数面向对象的程序设计系统,都是通过继承来提供多态的。也就是说,设想的 Flea 和 Tyrannosaur 类可能都是从某个 Animal 类继承来的。每个类都将重写 Animal 类的 Bite 方法,以提供自己的噬咬特点。 多态来自于这个事实:可以调用属于某类对象的 Bite 方法—该类可以是从 Animal 中派生出来的任意类,而不必知道该对象到底属于哪一个类。 提供多态接口
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 是如何提供多态的(1)”中所解释的,所谓接口,就是一组属性和方法。在下面的代码示例中,将创建一个 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_ 前缀来标识该接口。
重点 一个接口就象一个契约。实现接口后,当调用该接口的任何属性或者方法时,一个类已经约定好要作出反应。因此,必须实现接口的所有属性和方法。
'(跳过多少英寸的代码,略。)
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 方法。
将 Tyrannosaurs 和 Fleas 传递给过程
回忆一下“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 为了返回值的赋值,使用全过程名,包括接口前缀。