本文讨论VBA中的面向对象编程,并通过类封装对数组的操作。
面向对象编程(OOP,Object-Oriented Programming)的概念可以从两个角度来理解,首先,面向对象中的类型称为“类”,而某个类类型的变量称为“对象”,对象会指向一个类的实例;再者,类是基本数据类型及其操作代码耦合度更高的组织形式。
如前面的文章中对于数组的应用,可以看到,数组会单独定义,而数组的操作需要使用另外的函数,代码耦合度很低;那么,有没有更加便于记忆、可读性更高的代码组织形式呢?如向数组添加成员时不是使用ReDim Preserve语句,而是使用类似arr.Add(e)这样的代码。
VBA中的“类”是通过类模块文件实现,而不是通过Class关键字定义。首先,在“工程”窗口中通过鼠标右键菜单“插入”>>“类模块”添加一个新的类,并命名为“cArray”;可以在“属性”窗口修改“(名称)”的值;完成后工程窗口和属性窗口如下图所示。
在VBA中可定义的成员类型有字段(field)、属性(property)、方法(method)、事件(event),本文主要使用字段、属性和方法,事件在窗体和控件相关主题中会有讨论。
字段,也就是定义在类中的变量,用于保存基本的数据。
属性,用于读写数据,可以读写过程中做一些额外的工作,如检查数据正确性、对基础数据进行计算等。
方法,由子程序和函数实现,用于数据的操作封装。
事件,是一种在代码运行时动态插入代码的编程方法。比如,图形界面开发中常常会使用“按钮”,点击不同的按钮会执行不同的操作,此时可以将“点击”设置为一个事件,然后将不同的按钮对象的“点击”事件和对应的处理方法关联,从而实现各个按钮的点击操作。
此外,类的成员还可以设置访问级别,如使用Private关键字可以指定成员只能内部调用,使用Public关键字指定成员可以在外部调用。在类的成员中,字段(变量)的默认访问级别为Private,属性和方法的默认级别为Public。
接下来封装数组的操作,首先在cArray类模块中添加如下代码。
Option Base 0 Dim myArr() As Variant '返回数组成员数量 Property Get Count() As Long On Error GoTo CountError Count = UBound(myArr) + 1 Exit Property CountError: Count = 0 End Property ' 添加数组成员 Sub Add(val As Variant) n = Count ReDim Preserve myArr(n) myArr(n) = val End Sub ' 判断成员是否存在 Function Exists(val As Variant) As Boolean On Error GoTo ExistsError For Each e In myArr If e = val Then Exists = True Exit Function End If Next ExistsError: Exists = False End Function
代码中首先使用“Option Base 0”语句明确数组的索引从0开始;然后,定义了四个元素,分别是myArr变量、Count属性、Add()和Exists()方法,下面分别讨论。
myArr变量在cArray类的内部保存数组数据,其默认为Private访问级别,只能在cArray类的内部使用。
Count属性使用Property Get语句定义,其功能是定义了Count属性的读取操作,用于返回数组元素的数量。请注意其中的操作,当数组没有成员时,使用UBound函数获取最大索引时出现运行时错误,代码中正是利用了这一特性,当数组为空时返回数组元素数量为0;否则使用公式“最大索引+1”计算数组元素的数量。
Add()方法使用子程序实现,用于向数组添加元素,因为明确了数组的索引从0开始,所以新的元素索引就是原数组元素的数量,直接使用Count属性获取。
Exists()方法使用函数实现,用于判断数组中是否存在val参数指定的元素,存在时返回True,否则返回False。请注意,当数组为空时无法使用For Each语句遍历数组元素,所以需要错误捕捉代码,在For Each语句结构中访问数组元素时,如果发现指定的元素就立即指定函数返回True并退出函数;另外,在ExistsError:标签前并没有使用Exit Function语句退库函数,这是因为,当数组元素遍历完成或无法遍历数组元素时,函数都需要返回False。
下面的代码,在模块1中测试cArray类的使用。
Sub Main() Set arr = New cArray arr.Add ("a") arr.Add ("b") Debug.Print (arr.Count) Debug.Print (arr.Exists("a")) End Sub
执行代码会显示2和True。这里,使用Set和New关键字将arr对象定义为新的cArray类的实例。
接下来可以根据需要在cArray类中添加代码,如下面的代码添加了GetValue()和GetArray()方法。
' 根据索引返回数组元素 Function GetValue(index As Long) As Variant GetValue = myArr(index) End Function ' 返回数组 Function GetArray() GetArray = myArr End Function
其中,GetValue()方法会根据索引返回元素数据,而GetArray()方法会返回数组。下面的代码,我们在模块1中测试这两个方法的应用。
Sub Main() Set arr = New cArray arr.Add ("a") arr.Add ("b") Debug.Print (arr.GetValue(1)) ' For Each e In arr.GetArray() Debug.Print (e) Next End Sub
第一个Debug.Print()方法会显示第2个元素“b”,然后通过For Each语句访问了arr.GetArray()方法返回的数组的所有元素,这里会显示“a”和“b”。
前面的代码,在cArray类中使用Property Get语句结构定义了Count属性的读取操作,那么,如何定义属性的赋值操作呢?
属性的赋值和变量(对象)的赋值方式相同,需要区分基本数据类型变量和类类型的对象。前面的示例中,在给基本类型变量赋值时使用了类似如下代码。
Sub Main() Dim x As Long x = 10 y = 99 result = x + y Debug.Print (result) End Sub
代码中,变量的使用是非常灵活的,可以通过Dim语句声明x变量为Long类型,也可以不声明变量直接使用,如y和result。实际上,如Long这样的基本数据类型变量在赋值时需要使用Let关键字,只是Let关键字可以省略,如本例代码和下面的代码含义完全相同。
Sub Main() Dim x As Long Let x = 10 Let y = 99 Let result = x + y Debug.Print (result) End Sub
也许大家已经注意到了,在给对象赋值时使用了Set关键字,而Set关键字是不能省略的。在类中定义属性的赋值操作时也是这样,当属性的数据类型是基本的类型时使用Property Let语句结构,当属性的数据类型是类时则使用Property Set语句结构。下面创建一个简单的类进行测试。首先在“工程”窗口中插入cPerson类模块,并添加如下代码。
Private myName As String Private myPets As cArray Public Sub Class_Initialize() myName = "Noname" End Sub Public Property Get Name() As String Name = myName End Property Public Property Let Name(value As String) myName = value End Property Public Property Get Pets() As cArray Set Pets = myPets End Property Public Property Set Pets(ByRef value As cArray) Set myPets = value End Property
cPerson类中,首先定义了两个私有(Private)变量,分别保存姓名(myName)和宠物(myPets);接下来定义的Class_Initialize()方法可以在创建实例时自动调用;然后定义了Name和Pets属性的读取和赋值方法,这里,Name属性的赋值操作使用了Property Let语句结构,而Pets属性的赋值操作使用了Property Set语句结构。下面的代码,在模块1中测试cPerson类的使用。
Sub Main() Set p = New cPerson Debug.Print (p.Name) p.Name = "张三" Debug.Print (p.Name) ' Dim pet As New cArray pet.Add ("Dog") pet.Add ("Cat") Set p.Pets = pet ' For Each e In p.Pets.GetArray() Debug.Print (e) Next End Sub
执行代码会显示“Noname”、“张三”、“Dog”和“Cat”。