自己的对象、自己的集合

代码重用的许多(内部)方面

有两种主要的代码重用窗体—二进制和源代码。二进制代码重用,是通过创建和使用一个对象来实现的,而源代码重用,则是通过继承来实现的。但 Visual Basic 不支持。(源代码重用也可以通过复制和修改源代码来实现,但这种技术没有什么新意,而且存在许多人所共知的问题。)

Visual Basic 已成为二进制代码重用的先驱—控件就是典型的例子。通过将控件的实例放置到窗体上就可以重用该控件中的代码。这被称为包含(containment) 关系或者具有 (has-a) 关系;也就是说,该窗体包含或者具有CommandButton。

详细信息   包含关系在本章后面的“对象模型”作了讨论。

委派给实现的对象

Implements 提供了代码重用的一种强有力的新途径。可以实现一个抽象类(就象在“创建和实现接口”中所讨论的那样),或者也可以实现一个全功能类的接口。可以在外部对象(即,实现内部对象接口的那个对象)的 Initialize 事件中创建内部对象(即,所实现的对象)。

就象在“创建和实现接口”中所提到的那样,接口就象契约一样—必须在外部对象的类模块中实现内部对象接口的所有成员。不过,在委派内部对象的属性和方法为代表方面,可以有很多选择。在一个方法中,可能直接委派一个内部对象为代表,传递未更改的参数,而在另一种方法中,可能执行在调用该内部对象之前自己的部分代码,在第三种方法中仅仅执行自己的代码,而完全忽略内部对象!

例如,假设有一个 OneManBand 类和一个 Cacophony 类,这二者都产生声音。希望把 Cacophony 类的功能添加到 OneManBand 类中,并重用 Cacophony 类方法的一些实现。

'OneManBand 实现 Cacophony 接口。
Implements Cacophony
 
'保存引用的对象变量。
Private mcac As Cacophony
 
Private Sub Class_Initialize()
   '创建对象。
   Set mcac = New Cacophony
End Sub

现在,就可以在“对象”下拉菜单上选择 Cacophony,然后为 Cacophony 接口的方法获得过程模板。为了实现这些方法,可以委派给 Cacophony 对象。例如,Beep 方法可能看起来如下所示:

Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
   '委派给内部的 Cacophony 对象。
   Call mcac.Beep(Frequency, Duration)
End Sub

上面的实现是非常简单的。外部对象 (OneManBand) 直接委派给内部对象 (Cacophony),不作任何更改即可重用 Cacophony 对象的 Beep 方法。这是一件好事,但仅仅只是个开端。

Implements 语句对于代码重用来说,是一个非常强大的工具,因为它给予很大的灵活性。可能想更改 OneManBand 类的 Beep 方法的效果,方法是在对内部 Cacophony 对象的调用之前(或之后),插入自己的代码:

Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
   '撞击每一件东西升高八度。
   Frequency = Frequency * 2
   '基于 OneManBand 类的另一个属性,即 Staccato
   '分割每个嘟嘟声的持续时间。
   If Staccato Then Duration = Duration * 7 / 8
   Call mcac.Beep(Frequency, Duration)
   '甚至可以调用 OneManBand 的其它方法。
   If Staccato Then Pause(Duration * 1 / 8)
End Sub

对于这些方法来说,实现可能会直接委派内部 Cacophony 对象为代表,而对于另外一些来说,可能在委派之前或之后插入自己的代码—或者甚至完全忽略委派,而完全用自己的代码来实现一种方法。

因为 OneManBand 类实现 Cacophony 接口,所以可以将它和调用该接口的任何音乐应用程序一起来使用。其实现细节可从调用应用程序处隐藏起来,但是结果发出的声音都是自己的。

注意   COM 提供了另一个机制进行二进制代码重用,该机制叫凝聚。在集合中,无更改地重用一个完整的接口,而且该实现是由被凝聚的类的一个实例所提供的。Visual Basic 不支持这种代码重用的窗体。

这样一来不冗长乏味吗

写委派代码的确可能变得冗长乏味,如果大部分外部对象的属性和方法,只是简单地直接委派给相对应的内部对象的属性和方法的时候,尤其是这样。

如果有 Visual Basic 专业版或企业版,就可以使用“Visual Basic 扩展性”模型,创建自己的委派向导,使任务自动化,类似于专业版和企业版中的“类向导”一样。

详细信息   在部件软件中多态和多重接口的使用,在《部件工具指南》中,“创建 ActiveX 部件”中的“部件设计的一般准则”中作了讨论。

关于“扩展性模型”的使用,在《部件工具指南》中的“用外接程序来扩展 Visual Basic 环境”中作了文档说明。

用自己的对象编程

现在可以开始逐渐使用对象,寻找有用的一些任务—对这些任务来说,将代码和数据结合起来是一个优点。可以使用这些对象的功能,通过声明对象变量、将新对象赋给它们、并调用这些对象的属性和方法

随着越来越多的对象添加到自己的程序中,将会开始看到它们之间的关系。就可以开始进行更多依赖于对象及其之间关系的程序设计了,并可开始使用更强健的技巧—比如创建自定义集合类—这是为了在代码中表达那些关系。

在某些点上,将会突然发现将对象链接在一起是如何改变了程序本质,这样,就已经有了开始设计基于对象的程序的条件。

下面的主题,概述了编程风格中这些革命性改变。现在,读一读它们,给自己一个思考的图片,在对基于对象的程序设计的概念有所了解时,再读读它们。

  • 对象引用和引用计数   使用对象的数量越多,就越需要管理对象的引用,这样才能使部件在需要的时候把内存空出来,并以有序的方式关闭。
  • 对象模型   由自己的类创建的对象相互关系如何?对象模型提供了它们之间的包含关系—也就是说,一种对象包含一个或者数个另一种对象。这对于程序来说,是一个功能强大的结构原则。
  • 创建自己的集合类   提出有关 Visual Basic 的 Collection 对象的某些问题,并显示如何创建自己的集合类—也就是说,该类在提供增强的强健程度和更多功能的同时,委派一个 Collection 对象为代表(这样就获得了使用 For Each ... Next 的能力)。

详细信息   ActiveX 部件开创了代码重用和基于对象程序设计的另一新领域。如果有 Visual Basic 专业版或企业版,就可以通过《部件工具指南》中的“创建 ActiveX 部件”来开始探索这一新领域。

对象模型

通过创建类模块并赋以属性和方法,就可以定义了类,接着就可以由该类创建任意数量的对象。如何记录所创建的这些对象呢?

记录对象最简单的办法,莫过于为创建的每个对象都声明一个对象变量。当然,这样对能够创建对象的数量就有了限制。

可以在某个数组或者集合中保持多个对象引用,就象在本章前面的“创建对象数组”和“创建对象集合”中所讨论的那样。

刚开始时,可能要定位窗体或标准模块中的对象变量、对象数组以及对象集合,就跟处理普通变量一样。但是,随着添加更多的类,可能会发现正使用的这些对象之间有明确的关系。

《创建对象数组》内容可见

http://wenku.baidu.com/view/7543cb7e5acfa1c7aa00cc0a.html?pn=50

在第5页

c对象模型表达了包含关系

对象模型给出了基于对象程序的结构。通过定义程序中所使用的对象之间的关系,对象模型能够以一种使编程变得更容易的方式来组织对象。

一般来说,对象模型表达了这样一个事实:即某些对象是“更大的”,或者说比其它对象更重要一些—可以认为这些对象是包含其它对象的对象,或者是由其它对象所组成的对象。

例如,在编程时,可能创建一个 SmallBusiness 对象来作为程序的核心。可能想让这个 SmallBusiness 对象包含与其关联的其它类型的对象,比如 Employee 对象和 Customer 对象。可能同时也希望它包含一个 Product 对象。在图 9.11 中显示了该程序的对象模型。

图 9.11   对象模型

可以定义四个类模块,分别叫做 SmallBusiness、Employee、Customer 和 Product 类模块,并给它们中的每个添加合适的属性和方法,但是怎样在对象之间建立连接呢?有两种工具可以达到这个目的:即 Object 属性和 Collection 对象。下列这段代码显示了实现图 9.11 分层结构的一种办法。

' SmallBusiness 类模块中声明部分的代码。
Public Name As String
Public Product As New Product
Public Employees As New Collection
Public Customers As New Collection

第一次引用 Product 属性时,将创建对象,因为已经把它声明为 As New。例如,可以用下面的代码创建并设置 SmallBusiness 对象的 Product 对象的名称和价格。

' 一个标准模块的代码。
Public sbMain As New SmallBusiness
Sub Main
   sbMain.Name = "Velociraptor Enterprises, Inc."
   ' 在代码中首次使用 Product 变量时,创建 Product 对象。
   sbMain.Product.Name = "Inflatable Velociraptor"
   sbMain.Product.Price = 1.98
   .
   .   ' 初始化并显示主窗体的代码。
   .
End Sub

注意   用公共变量来实现一个对象属性并不简洁。如果在代码中的某处,将该属性设置为 Nothing 的话,可能会无意中将 Product 对象撤消。更好的办法是创建对象属性时,将之设置为只读属性,如下列代码所示。

'为更强健的对象属性所编制的代码。该属性的存储是私有的,
'因此不能从对象的外部将之设置为 Nothing
Private mProduct As New Product
 
Property Get Product() As Product
   '首次调用这个属性时,mProduct 包含 Nothing
   '因此 Visual Basic 将创建一个 Product 对象。
   Set Product = mProduct
End If

一对多对象关系

当对象之间关系是一对一时,对象属性可以正常工作。然而,经常出现的情况却是,某种类型的一个对象包含另一种类型的一些对象。在 SmallBusiness 对象模型中,Employees 属性是作为一个 Collection 对象来实现的,因此,这个 SmallBusiness 对象可以包含多个 Employee 对象。下列代码显示了怎样把新的 Employee 对象添加到这个集合中。

Public Function NewEmployee(Name, Salary, HireDate, _
ID) As Employee
   Dim empNew As New Employee
   empNew.Name = Name      '对象的隐含创建。
   empNew.Salary = Salary
   empNew.HireDate = HireDate
   '添加到集合中,并使用 ID 作为一个键。
   sbMain.Employees.Add empNew, CStr(ID)
   '返回对新 Employee 的引用。
   Set NewEmployee = empNew
End Function

在创建 SmallBusiness 对象所代表的企业雇员时,需要调用这个 NewEmployee 函数多少次,就可以调用多少次。任何时候,通过遍历 Employees 集合,都可将现有的雇员列出。

注意   再次说明,并没有非常强健的实现方法。比较好的实践是创建自己的集合类,并将它们按只读属性给出。这在“创建自己的集合类”中作了讨论。

提示   在 Visual Basic 的专业版和企业版中,包括 Class Builder 实用工具,利用这个实用工具,可以产生实现对象模型时需要的大量代码。Class Builder 可创建强健的对象属性和集合类,并且可以很容易地重新组织对象模型。

Parent 属性

具有对象的引用时,通过使用对象属性和对象集合,就可以到达这个引用所包含的对象。能够向分层结构的上层移动也是非常有用的,因为可以到达包含所引用对象的那个对象,。

向上移动通常是用 Parent 属性来完成的。Parent 属性返回对象容器的引用。关于对象模型定位的讨论,请参阅“用部件编程”中的“定位对象模型”。

可以在本章前面的“向类中添加属性”找到使用 Parent 属性的示例。

提示   当把 Parent 属性赋给集合中的对象时,不要使用对 Collection 对象的引用。这个对象真正的父是包含该集合的对象。如果 Parent 属性指向了该集合,将不得不使用两级间接指针,才能到达真正的父—也就是说,要用obj.Parent.Parent,而不是obj.Parent

Parent 属性、循环引用,以及对象拆卸

使用 Parent 属性的一个最大问题是它们会造成循环引用。“较大”的对象具有对所包含对象的引用,而被包含的对象通过其 Parent 属性也有引用,这样就创建了一个环,如图 9.12 所示。

图 9.12   循环引用的一个例子

在这幅图片上有什么错误?去除这些对象的办法,是在用对象完成工作后,释放所有对它们的引用。假设对 SmallBusiness 对象的引用是在一个名为sbMain 的变量中,就象在本节主题前面讨论的那样,可能会写出象下面这样的代码:

Set sbMain = Nothing

不幸的是,仍然有一个对 SmallBusiness 对象的引用—事实上,可能有许多引用,因为每个 Employee 对象的 Parent 属性都将包含对这个 SmallBusiness 对象的引用。

由于 SmallBusiness 对象的 Employees 集合包含有对每个 Employee 对象的引用,因此任何对象都不会被撤消。

TearDown 方法

一个解决这个问题的办法是,把 TearDown 方法添加给该 SmallBusiness 对象。这样,就可以将所有 SmallBusiness 对象的对象属性设置为 Nothing,同时也将所有 Collection 对象 (Employees, Customers) 设置为 Nothing。

当 Collection 对象被撤消时,Visual Basic 就把它所包含的所有对象引用设置为 Nothing。如果没有对包含在 Employees 和 Customers 集合中的 Employee 和 Customer 对象其它的引用,那么它们将被撤消。

当然,如果 Employee 对象是由更小的对象构成的,则将会存在其父对象所具有的同样的循环引用问题。在那种情况下,必须将一个 TearDown 方法赋给 Employee 类。要做的事情并不仅仅是将 Employees 的 Collection 对象设置为 Nothing,SmallBusiness 对象还将不得不首先遍历该集合,调用每个 Employee 对象的 TearDown 方法。

其它问题

即使这样,也并非所有对象都能被撤消。如果在程序的某个地方还有一些变量,这些变量仍然包含对 SmallBusiness 对象,或者对 SmallBusiness 对象所包含的任何对象的引用,那么这些对象将不会被破坏。程序中必须有一部分清理代码,以确保各个地方的所有对象变量都被设置为 Nothing。

为了测试是否正发生这种情况,可能需要将一些调试代码添加到对象中。例如,可以将下列代码添加到标准模块中:

' 全局调试集合
Public gcolDebug As New Collection
 
' 全局函数,赋给每个对象一个唯一的 ID
Public Function DebugSerial() As Long
   Static lngSerial As Long
   lngSerial = lngSerial + 1
   DebugSerial = lngSerial
End Function

在每个类模块中,都可以加入类似下面这样的代码。每个类在“Product”出现的地方提供它自己的名字。

'调试 ID 的存储。
Private mlngDebugID As Long
 
Property Get DebugID() As Long
   DebugID = mlngDebugID
End Property
 
Private Sub Class_Initialize()
   mlngDebugID = DebugSerial
   '将一个字符串登录项添加到全局集合中。
   gcolDebug.Add "Product Initialize; DebugID=" _
   & DebugID, CStr(DebugID)
End Sub
 
Private Sub Class_Terminate()
   '删除字符串登录项,这样,就会知道对象不再存在了。
   gcolDebug.Remove CStr(DebugID)
End Sub

随着每个对象的创建,该对象将一个字符串放到全局集合中;当该对象撤消时,将删除该字符串。在任何时候,都可以遍历全局集合,看看什么对象还没有撤消。

详细信息   用 Visual Basic 的专业版或者企业版来创建 ActiveX 部件时,对象模型承担新的重要性,并且是另一个问题集合。请参阅《部件工具指南》中,“创建 ActiveX 部件”中的“部件设计的一般准则”。

创建自己的集合类

般可以采取三种办法用集合来实现对象包含。以上面“对象模型”中所讨论的 SmallBusiness 对象的 Employees 集合为例。为了实现该集合,可以这样做:

  • 在 SmallBusiness 类模块中,将 Employees 变量声明为 As Collection,并使之成为 Public。这是最简单的解决方案。
  • 在 SmallBusiness 类模块中,将 mcolEmployees 变量声明为 As Collection,并使之成为 Private。将添加和删除对象的一组方法赋给 SmallBusiness 对象。这是三种设计方案中面向对象概念用得最少的办法。
  • 通过创建名为 Employees 的集合类模块,来实现自己的集合类,就象在本章后面所描述的那样。将 Employees 类的只读属性赋给 SmallBusiness 对象。

下面以强健性的逐渐增加为顺序,列出了三种策略。它们的特点可以分别用稻草盖的房子、树枝盖的房子和砖块盖的房子来作类比。

公有集合示例:稻草盖的房子

为了创建此示例,打开一个新工程并插入两个类模块。在窗体上画出五个命令按钮、一个列表框、两个文本框,以及两个标签,如图 9.13 所示。

图 9.13   Employees 集合示例

下表列出了该示例所需设置的属性值。

对象

属性

设置值

类模块

Name

Employee

类模块

Name

SmallBusiness

窗体

Caption

雇员集合

第一个命令按钮

Caption
Name

添加
cmdAddEmployee

第二个命令按钮

Caption
Name

删除
cmdDeleteEmployee

第三个命令按钮

Caption
Name

刷新表
cmdListEmployees

第四个命令按钮

Caption
Name

有问题
cmdTrouble

第五个命令按钮

Caption
Name

关闭
cmdClose

第一个标签控件

Caption

Name

第二个标签控件

Caption

Salary

第一个文本框

Name
Text

txtName
(blank)

第二个文本框

Name
Text

txtSalary
(blank)

列表框

Name

lstEmployees

 

在 Employee 类模块中,将下面的声明和属性过程添加进去:

Option Explicit
'Employee 类的属性。
Public Name As String
Public Salary As Long   
 
'只写一次 ID 属性的私有数据。
Private mstrID As String
 
Property Get ID() As String
   ID = mstrID
End Property
 
'首次设置 ID 属性时,静态的 Boolean 变量也要设置。
'后续的调用什么也不做。
'(若不是这样,产生一个错误会更好。)
Property Let ID(strNew As String)
   Static blnAlreadySet As Boolean
   If Not blnAlreadySet Then
      blnAlreadySet = True
      mstrID = strNew
   End If
End Property

ID 属性是从集合中检索或者删除 Employee 对象的键,因此必须对它进行一次性设置并不再更改。这是用 Static Boolean 变量来完成的,该变量在首次设置 ID 属性的时候被设置为 True。该属性总是可读的,因为有一个 Property Get。

在 SmallBusiness 类模块中,将下面的声明添加进去。当代码中引用Employees 变量时,将创建集合对象。

Option Explicit
Public Employees As New Collection

窗体所做的所有工作

所有的剩余代码都在窗体模块中。在声明部分添加下面的声明。

Option Explicit
Public sbMain As New SmallBusiness

cmdEmployeeAdd_Click 事件中的代码,将一个成员添加到集合。

Private Sub cmdEmployeeAdd_Click()
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' With 语句,使代码更快,也更简洁
   '.ID 对应 empNew.ID)
   With empNew
      '为新的雇员产生一个唯一的 ID
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = txtName.Text
      .Salary = CDbl(txtSalary.Text)
      ' Employee 对象引用添加到集合中,
      ' ID 属性作为键。
      sbMain.Employees.Add empNew, .ID
   End With
   txtName.Text = ""
   txtSalary.Text = ""
   '单击“刷新表”按钮。
   cmdListEmployees.Value = True
End Sub

在 cmdListEmployees_Click 事件过程中的代码,用一个 For Each ... Next 语句将所有的雇员信息添加到 Listbox 控件中。

Private Sub cmdListEmployees_Click()
   Dim emp As Employee
   lstEmployees.Clear
   For Each emp In sbMain.Employees
      lstEmployees.AddItem emp.ID & ", " & emp.Name _
      & ", " & emp.Salary
   Next
End Sub

cmdEmployeeDelete_Click 事件用 Collection 对象的 Remove 方法,删除当前 ListBox 控件中选定的集合对象。

Private Sub cmdEmployeeDelete_Click()
   '检查一下,以确保有被选中的雇员。
   If lstEmployees.ListIndex > -1 Then
      '前六个字符是 ID
      sbMain.Employees.Remove _
      Left(lstEmployees.Text, 6)
   End If
   '单击“刷新表”按钮。
   cmdListEmployees.Value = True
End Sub

将下面的代码添加给“有问题”按钮。

Private Sub cmdTrouble_Click()
   '说些什么!?
   sbMain.Employees.Add Me
End Sub

cmdClose_Click 事件关闭该应用程序。当关闭使用对象的工程时,卸载所有窗体,确保类模块中的所有 Terminate 事件过程都将得到执行。相反,使用 End 语句终止程序显得生硬,不会执行 Terminate 事件。

Private Sub cmdClose_Click()
   Unload Me
End Sub

在示例中,为了添加雇员,可以运行应用程序,在两个文本框中输入值,然后选取“添加”按钮。添加几个雇员,然后用删除和列表按钮来作一下实验。

强健性相当于稻草盖的房子

这种简单的实现方法不是很强健。因为 Employees 属性仅仅是一个公有的 Collection 对象,可能无意中在程序中的任何地方访问它。而且,Collection 对象的 Add 方法不作任何类型检查。例如,在“有问题”按钮的 Click 事件中的代码,会轻率地将对窗体的对象引用插入到雇员集合中。

单击“有问题”按钮,会注意到未出现任何错误。现在,单击“刷新表”按钮。当 For Each ... Next 循环遇到意外的对象类型时,它将导致类型为 13 的错误发生,即“类型不匹配”。

这是用公用的 Collection 对象建立对象模型时,可能会碰到的错误的一个示例。可以从工程的任何地方添加对象,而且对于它们是否将被正确地初始化,没有任何保证。如果程序员复制代码以添加雇员,而且原始代码后来又有所改动,那么要找到产生的错误非常困难。

详细信息   本主题中开始探讨的这个示例,在下面的“私有集合示例:树枝盖的房子”中将继续讨论。

私有集合示例:树枝盖的房子

在本主题中,继续讨论在上面的“公有集合示例:稻草盖的房子”中开始探讨的代码示例。在开始本主题之前,最好阅读上面的主题。

将 Employee 对象和 SmallBusiness 对象连接起来的一种更强健一些的办法是使 Collection 对象私有化。对于该示例来说,将要从上面的“公有集合”示例中重用窗体和大部分代码。

Employee 类模块没有改变。然而,SmallBusiness 类模块被完全翻新了。用下面的声明语句来取代公有的 Collection 对象的声明,并把下面几个段落中所描述的 Sub 和 Function 过程添加进去。

Option Explicit
Private mcolEmployees As New Collection

跟前面的一样,添加一个雇员的代码做大部分工作。(可以从前面示例中的 cmdEmployeeAdd_Click 事件过程中把点线之间的代码块取出来。)

重要的改动在于:Collection 对象的 Add 方法不能再从程序的任意模块来调用了,因为mcolEmployees 是 Private。只能用 EmployeeAdd 方法来添加 Employee 对象,这将会使新对象正确地初始化:

'SmallBusiness 的类方法。
Public Function EmployeeAdd(ByVal Name As String, _
ByVal Salary As Double) As Employee
   ' - - - - - - - - - - - - - - - -
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' With 语句,使代码更快,也更简洁
   '.ID 对应 empNew.ID)
   With empNew
      '为新雇员产生一个唯一的 ID
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = Name
      .Salary = Salary
      ' Employee 对象引用添加到集合中,
      ' ID 属性作为键。
      ' - - - - - - - - - - - - - - - -
      mcolEmployees.Add empNew, .ID
   End With
   '给新雇员返回一个引用。
   Set EmployeeAdd = empNew
End Function

EmployeeAdd 方法为新添加的 Employee 对象返回一个引用。这是一个好的实践,因为一旦创建了一个对象,很可能就想用它来做点什么事。

EmployeeCount、EmployeeDelete 以及 Employees 方法委派 Collection 对象的相应方法为代表。委派意味着 Collection 对象做全部工作。

'SmallBusiness 类的方法。
Public Function EmployeeCount() As Long
   EmployeeCount = mcolEmployees.Count
End Function
 
Public Sub EmployeeDelete(ByVal Index As Variant)
   mcolEmployees.Remove Index
End Sub
 
Public Function Employees(ByVal Index As Variant) _
As Employee
   Set Employees = mcolEmployees.Item(Index)
End Function

注意   可以将附加的功能添加到这些方法中。例如,如果一条索引项是无效的,能够引发自己的错误。

最后的方法是 Trouble。该方法试图将未初始化的 Employee 对象添加到集合中。能猜测到将会发生什么事吗?

'SmallBusiness 类的方法。
Public Sub Trouble()
   Dim x As New Employee
   mcolEmployees.Add x
End Sub

对窗体的更改

现在,将不得不对窗体模块作一些改动。可以使用前面示例所使用的同样的模块级声明,而且“关闭”按钮的 Click 事件也是相同的,但是,其它事件过程已经改变了—“添加”按钮的代码短了许多,而“删除”和“列出雇员的表”按钮的代码则已经改变为一种很小但却很有效的方式:

Private Sub cmdEmployeeAdd_Click()
   sbMain.EmployeeAdd txtName.Text, txtSalary.Text
   txtName.Text = ""
   txtSalary.Text = ""
   cmdListEmployees.Value = True
End Sub
 
Private Sub cmdEmployeeDelete_Click()
   '检查一下,以确保有雇员被选中。
   If lstEmployees.ListIndex > -1 Then
      '前六个字符是 ID
      sbMain.EmployeeDelete Left(lstEmployees.Text, 6)
   End If
   cmdListEmployees.Value = True
End Sub
 
Private Sub cmdListEmployees_Click()
   Dim lngCt As Long
   lstEmployees.Clear
   For lngCt = 1 To sbMain.EmployeeCount
      With sbMain.Employees(lngCt)
         lstEmployees.AddItem .ID & ", " & .Name _
         & ", " & .Salary
      End With
   Next
End Sub

但是,在 cmdListEmployees_Click 中的所有这些附加代码是什么?不幸的是,在追求强健性的过程中,就已经放弃了使用 For Each ... Next 来遍历集合中各项的能力,因为现在 Collection 对象已经声明为 Private。如果试图象下面这样编写代码,那么将会产生错误:

'不会正常工作,因为 Employees 不是一个真正的集合。
For Each emp In sbMain.Employees

幸运的是,可以用 EmployeeCount 方法来为遍历范围分界。

“有问题”按钮也作了少许更改,但它仍然是“有问题”。

Private Sub cmdTrouble_Click()
   sbMain.Trouble
End Sub

运行该工程,并用“添加”、“删除”以及“刷新表”按钮作一下试验。每件事情工作起来,都跟以前是一样的。

当单击“有问题”按钮时,还是不产生任何错误。但是,如果现在单击“刷新表”按钮的话,就可以看到未初始化的 Employee 对象已经以某种方式被添加到集合中了。

怎么会发生这种情况?使 Collection 对象私有化以后,就保护它不受程序中所有在 SmallBusiness 对象外部的代码的影响了,但并没有防范内部代码的影响。在这种情况下,SmallBusiness 对象就可能由于内部有大量代码而变得很大,而且很复杂。例如,它将很可能具有象 CustomerAdd、ProductAdd,等等这些方法。

代码错误,或者 EmployeeAdd 方法的重复创建,仍然可能造成错误数据—甚至无效的对象—被插入到集合中,因为私有的变量在整个类模块中都是可见的。

详细信息   该示例在“创建自己的集合类:砖块盖的房子”中将继续讨论。

创建自己的集合类:砖块盖的房子

本主题将继续讨论在“公有集合示例:稻草盖的房子”和“私有集合示例:树枝盖的房子”开始讨论的代码示例。在开始本主题之前,最好阅读那些主题。

实现集合的最强健方法,是使其成为类模块。与前面的示例相比较,将对象创建的所有代码移到集合类中,这就较好符合了面向对象程序设计的原则。

该例使用了跟前面的例子同样的窗体和同样的 Employee 类模块。插入新的类模块,并将其 Name 属性设置为“Employees”。将下面的声明和代码插入到这个新的类模块中。

Option Explicit
Private mcolEmployees As New Collection

Employees 类的 Add、Count 以及 Delete 方法,本质上都跟旧的 SmallBusiness 类中的那些方法是相同的。可以简单地把它们从 SmallBusiness 类模块中删除,然后粘贴到 Employees 类模块中,并更改一下它们的名字。

名字都可以更改,因为不再需要将 EmployeeAdd,比如说,跟 CustomerAdd 区别开来。所实现的每个集合类都有自己的 Add 方法。

'Employees 集合类的方法。
Public Function Add(ByVal Name As String, _
ByVal Salary As Double) As Employee
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' With 语句,使代码更快,也更简洁
   '.ID 相对 empNew.ID)
   With empNew
      '为新雇员产生一个唯一的 ID
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = Name
      .Salary = Salary
      ' Employee 对象引用添加到集合中,
      ' ID 属性作为键。
      mcolEmployees.Add empNew, .ID
   End With
   '为新的 Employee 返回一个引用。
   Set Add = empNew
End Function
 
Public Function Count() As Long
   Count = mcolEmployees.Count
End Function
 
Public Sub Delete(ByVal Index As Variant)
   mcolEmployees.Remove Index
End Sub

SmallBusiness 对象的 Employees 方法变成了集合类中的 Item 方法。为了通过索引或者键来检索成员,它仍然委派 Collection 对象为代表。

'Employees 集合类的方法。
Public Function Item(ByVal Index As Variant) _
As Employee
   Set Item = mcolEmployees.Item(Index)
End Function

可以在这里添加一个很好的技巧。通过将 Item 设置为 Employees 类的缺省方法,就得到了为Employees("E00001") 编写代码的能力,就象用 Collection 对象做的一样。

要使 Item 成为缺省的属性,请按照以下步骤执行:

  1. 工具菜单上,单击过程属性,打开过程属性对话框。在名称框中,选择Item方法。
  2. 单击高级,显示高级功能。在过程标识符框中,选择(缺省),使 Item 方法成为缺省的。单击确定

注意   一个类只能有一个缺省成员(属性或者方法)。

使 For Each ... Next有效

伴随着强健性而来的,是重新获得了 For Each ... Next。通过添加下面的方法,可以再一次将所有工作委派给 Collection 对象:

'NewEnum 必须返回一个集合的枚举算子的 IUnknown 接口。
Public Function NewEnum() As IUnknown
   Set NewEnum = mcolEmployees.[_NewEnum]
End Function

委派给 Collection 对象的最重要的东西是它的枚举算子。枚举算子是一个小对象,该对象知道如何遍历集合中的各项。不能用 Visual Basic 来书写枚举算子对象,因为 Employees 类是基于 Collection 对象的,可以返回 Collection 对象的枚举算子—它自然知道如何枚举 Collection 对象所保存的各项。

包围 Collection 对象的 _NewEnum 方法的方括号是必要的,这是因为方法名前面的下划线。前面的这个下划线是一个约定,它指明该方法是隐藏在类型库中的。不能把自己的方法命名为 _NewEnum,但是可以把它隐藏在类型库中,并将 For Each ... Next 所需要的过程 ID 赋给它。

要隐藏 NewEnum 方法,并将所需的过程 ID 赋给它,请按照以下步骤执行:

  1. 工具菜单上,单击过程属性,打开过程属性对话框,在名称框中,选择 NewEnum 方法。
  2. 单击高级,显示一些高级功能。选中隐藏该成员,使 NewEnum 隐藏在类型库中。
  3. 过程标识符框中,键入-4,将 For Each ... Next所需要的过程标识符赋给 NewEnum。单击确定

重点   为了使自己的集合类使用 For Each ... Next 来工作,必须提供有正确过程标识符的隐藏 NewEnum 方法。

SmallBusiness 类留下的东西不多了

到现在为止,SmallBusiness 类中还只有相当少的代码。为了取代所删除了的 Collection 对象和所有方法,有新的声明和只读属性:

Option Explicit
Private mEmployees As New Employees
 
Public Property Get Employees() As Employees
   Set Employees = mEmployees
End If

这需要简单解释一下。假设曾经省略了 Property Get,那么可以简单地声明Public Employees As New Employees

只要没有人犯什么错误,每件事物都将很好地工作,但是如果偶然编写代码为Set sbMain.Employees = Nothing,将会发生什么情况?对,Employees 集合将被撤消。若是将 Employees 设置为只读属性,则可以避免这种可能性。

对窗体的改动

窗体模块的代码跟前面示例中的非常类似。可用模块级相同的声明,而且“关闭”按钮的 Click 事件也是同样的。

在大多数事件过程中的唯一改动是:用 Employees 集合对象的新方法取代 SmallBusiness 类的旧方法:

Private Sub cmdEmployeeAdd_Click()
   sbMain.Employees.Add txtName.Text, txtSalary.Text
   txtName.Text = ""
   txtSalary.Text = ""
   cmdListEmployees.Value = True
End Sub
 
Private Sub cmdEmployeeDelete_Click()
   '检查一下,以确保有雇员被选中。
   If lstEmployees.ListIndex > -1 Then
      '前六个字符是 ID
      sbMain.Employees.Delete _
      Left(lstEmployees.Text, 6)
   End If
   cmdListEmployees.Value = True
End Sub
 
Private Sub cmdListEmployees_Click()
   Dim emp As Employee
   lstEmployees.Clear
   For Each emp In sbMain.Employees
      lstEmployees.AddItem emp.ID & ", " & emp.Name _
      & ", " & emp.Salary
   Next
End Sub

注意,又可以用 For Each ... Next 来列出雇员了。

运行该工程,并验证每件事物都能正常工作。这次“有问题”按钮没有任何代码,因为封装已经消除了问题。

详细信息   为了得到关于背景集合的信息,请阅读“Visual Basic 集合对象”和“Visual Basic 集合”。

在专业版和企业版中,包括了 Class Builder 实用工具,该实用工具可以创建集合类。

稻草盖的房子、树枝盖的房子以及砖块盖的房子的示例课程部分,在下面的“好的面向对象程序设计的优点”中作了总结。

好的面向对象程序设计的优点

本主题是对开始于“公有集合示例:稻草盖的房子”一节,并在“私有集合示例:树枝盖的房子”和“创建自己的集合类:砖块盖的房子”一节中继续作了讨论的代码示例的结果进行总结。在开始本主题之前,请先阅读一下那些主题。

创建 Employees 集合类,其结果是产生了一种非常清晰、模块化的编程风格。这个集合的所有代码都在集合类中(封装),从而减小了 SmallBusiness 类模块的大小。如果 Employee 对象的集合出现在对象分层结构的不止一处,那么重用集合类将不需要复制任何代码。

增强集合类

可以为自己的集合类实现附加的方法和属性。例如,可以实现 Copy 和 Move 方法,或者包含 SmallBusiness 对象引用的只读 Parent 属性。

也可以添加事件。例如,每当 Add 或者 Remove 方法更改集合中的项数时,可以产生一个 CountChanged 事件。

强健性,强健性,强健性

并不总是必须以可能的最强健的方式来实现集合。不过,面向对象编程的一个优点是代码重用;重用对象比起复制源代码来要容易的多,而且如果使用的是强健的、封装的代码,则会更安全些。

一位精明的人说过:“如果想书写真正强健的代码,一定要假设会发生真正糟糕的事情。”

集合类和部件软件

如果使用的是 Visual Basic 的专业版或者企业版,可以将自己的工程转变成 ActiveX 部件,这样,同组的其他程序员就可以使用已经创建的对象了。

实现集合类的步骤

下面的清单总结了创建集合类所需要的步骤。

  1. 将类模块添加到工程中,并为它起一个名字通常是该集合类将包含的对象名的复数形式。(请参阅本章前面的命名属性、方法和事件一节。)
  2. 将私有变量添加进去,该私有变量包含对属性和方法所委派的 Collection对象的引用。
  3. Class_Initialize事件过程中,创建 Collection对象。(如果希望将该对象的创建时间推迟到需要时,可以在第二步中将私有变量声明为 AS New Collection。这样每次访问 Collection时会增加少量开销。)
  4. Count属性和 AddItem以及 Remove方法添加到类模块中;在每种情况下,通过调用其相应的成员,委派给私有的 Collection
  5. 实现 Add方法时,通过只接受一种类型的对象,可以覆盖 Collection对象的无鉴别力的 Add方法。甚至可以不让外部创建的对象添加到自己的集合中,这样Add方法就完全控制了对象的创建和初始化。
  6. 过程属性对话框,使“Item”方法成为集合类中缺省的方法。
  7. 象下面显示的那样,添加一个“NewEnum”方法。用过程属性对话框来将其标志为隐藏的,并将一个值为 -4 的过程标识符给它,这样,它就可以使用 For Each ... Next
8.           Public Function NewEnum() As IUnknown
9.           Set NewEnum = mcol.[_NewEnum]
10.       End Function

注意   上面的代码假设第二步中的私有变量被命名为mcol

  1. 将自定义属性、方法和事件添加到集合类中。

注意   在 Visual Basic 的专业版和企业版所包括的 Class Builder 实用工具,可用来创建集合类。就可以定制所得到的源代码。

详细信息   可以在《部件工具指南》中的“创建 ActiveX 部件”中读到更多关于软件部件的信息。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值