VBA编程——面向对象编程和数组操作封装

本文讨论VBA中的面向对象编程,并通过类封装对数组的操作。

VBA中的面向对象编程

面向对象编程(OOP,Object-Oriented Programming)的概念可以从两个角度来理解,首先,面向对象中的类型称为“类”,而某个类类型的变量称为“对象”,对象会指向一个类的实例;再者,类是基本数据类型及其操作代码耦合度更高的组织形式。

如前面的文章中对于数组的应用,可以看到,数组会单独定义,而数组的操作需要使用另外的函数,代码耦合度很低;那么,有没有更加便于记忆、可读性更高的代码组织形式呢?如向数组添加成员时不是使用ReDim Preserve语句,而是使用类似arr.Add(e)这样的代码。

VBA中的“类”是通过类模块文件实现,而不是通过Class关键字定义。首先,在“工程”窗口中通过鼠标右键菜单“插入”>>“类模块”添加一个新的类,并命名为“cArray”;可以在“属性”窗口修改“(名称)”的值;完成后工程窗口和属性窗口如下图所示。

添加类模块

在VBA中可定义的成员类型有字段(field)、属性(property)、方法(method)、事件(event),本文主要使用字段、属性和方法,事件在窗体和控件相关主题中会有讨论。

字段,也就是定义在类中的变量,用于保存基本的数据。

属性,用于读写数据,可以读写过程中做一些额外的工作,如检查数据正确性、对基础数据进行计算等。

方法,由子程序和函数实现,用于数据的操作封装。

事件,是一种在代码运行时动态插入代码的编程方法。比如,图形界面开发中常常会使用“按钮”,点击不同的按钮会执行不同的操作,此时可以将“点击”设置为一个事件,然后将不同的按钮对象的“点击”事件和对应的处理方法关联,从而实现各个按钮的点击操作。

此外,类的成员还可以设置访问级别,如使用Private关键字可以指定成员只能内部调用,使用Public关键字指定成员可以在外部调用。在类的成员中,字段(变量)的默认访问级别为Private,属性和方法的默认级别为Public。

封装数组操作

接下来封装数组的操作,首先在cArray类模块中添加如下代码。

VBA
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类的使用。

VBA
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()方法。

VBA
' 根据索引返回数组元素
Function GetValue(index As Long) As Variant
    GetValue = myArr(index)
End Function

' 返回数组
Function GetArray()
    GetArray = myArr
End Function

其中,GetValue()方法会根据索引返回元素数据,而GetArray()方法会返回数组。下面的代码,我们在模块1中测试这两个方法的应用。

VBA
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属性的读取操作,那么,如何定义属性的赋值操作呢?

属性的赋值和变量(对象)的赋值方式相同,需要区分基本数据类型变量和类类型的对象。前面的示例中,在给基本类型变量赋值时使用了类似如下代码。

VBA
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关键字可以省略,如本例代码和下面的代码含义完全相同。

VBA
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类模块,并添加如下代码。

VBA
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类的使用。

VBA
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”。