VBA 类模块系列之九——类中的常量

我们的第一个类的羽翼逐渐丰满,本篇介绍的内容将让它变得更加专业。 不知道你注意到没有,Person类中对性别( … 继续阅读“VBA 类模块系列之九——类中的常量”

我们的第一个类的羽翼逐渐丰满,本篇介绍的内容将让它变得更加专业。

不知道你注意到没有,Person类中对性别(Gender)属性的设计存在一点缺陷。我们用私有模块级字符串变量mstrGender来保存性别数据,既然是一个字符串变量,那就可以往里面保存任何字符串数据,比如“女”,“女性”,“Female”。虽然字面上都是同一个意思,但是写法上都不相同,你无法预料类的使用者会给mstrGender赋什么值,这样就会造成信息处理和判断上的混乱。作为一个专业的类的设计者,你必须提供一套规范的取值范围。

作为一个有一定经验的Access VBA开发人员,你肯定写过类似docmd.OpenForm “Form1”, acNormal 的语句来以正常的方式打开一个窗体。’acNormal’就是一个常量。我们在写这条语句的时候,IDE还会弹出智能窗口帮助我们选择相关的常量,如下图所示:

上图列示出来的7个常量,你通过常量名就知道各个常量的用意,另外你多半不会自己敲入一个不在这个列表中的其他常量。

这种在类中使用常量的方式是非常普遍的。你应该在很多地方已经看到过。现在我们就来介绍如何在自己的类中使用这种常量。

这种常量实际上是枚举类型(Enumeration)中的一个值。我们在Person类的头部,为性别属性定义一个枚举类型pGender

Public Enum pGender
    Female
    Male
End Enum

为什么叫pGender?“p”代表类名“Person”,为什么要将类名缩写作为前缀?因为我们定义的是公有的(Public)枚举类型,这个枚举类型在整个项目范围内都是可见的,将类名缩写作为前缀有利于与其他类中的枚举类型相互区分。

为什么Female(女性)/ Male(男性)这两个常量没有给一个常量的值?比如Female = “女”?呃,是的,一般的常量在定义的时候,需要给定一个常量值,可以为各种类型,比如Const PI As Double = 3.14,就是双精度型。但是枚举类型常量很特殊,它只能是长整形(Long)比如Female = 0。但是我们这里连“=0”都没有写。如果什么都没写,叫表示“从0开始,依次加1”,所以上述代码与下述代码是一样的

Public Enum pGender
    Female = 0
    Male = 1
End Enum

你可以在立即窗口中测试一下“?pGender.Female”。当然如果你有特殊考量,你可以让Female等于其他的值,比如3,如果没给Male设置值,那么Male就在3的基础上加1,等于4。

好了,枚举类型的定义就介绍到这里。我们来看看如何使用它。

有了pGender这个类型,我们就可以将性别属性定义为这种类型了。我们将Private mstrGender As String 改为Private menumGender As pGender (enum是枚举类型Enumeration的缩写),将Gender的读写属性做相应修改,相关代码最后效果如下:

'
'中间代码被忽略
'
Private menumGender As pGender
'
'中间代码被忽略
'
Public Enum pGender
    Female
    Male
End Enum
'
'中间代码被忽略
'
Public Property Get Gender() As pGender
    Gender = menumGender
End Property

Public Property Let Gender(lenumGender As pGender)
    Static blnFlag As Boolean
    If blnFlag = False Then
        blnFlag = True
        menumGender = lenumGender
    Else
        MsgBox "性别一旦设定,就无法修改"
    End If
End Property
'
'中间代码被忽略
'

有了这些修改以后,我们在类的测试代码中,就会发现不一样的效果了,如下图

这样,类的使用者(模块1中的测试代码)在给性别赋值的时候,IDE的智能提示就会自动显示出来,供其选择。

类中的枚举常量使用起来非常简单,但是它极大的增加了类的友好性,也是开发人员专业性的直观体现。是一项投资回报率非常高的技能。

最后,按惯例,将最终全部代码贴给大家。

'Person 类代码
Option Compare Database
Option Explicit

Private mstrName As String
Private menumGender As pGender
Private mstrSituation As String
Private mdatDOB As Date

Public Enum pGender
    Female = 0
    Male = 1
End Enum

Public Sub Speak()
    Select Case Situation
        Case "一般"
            MsgBox Left(Name, 1) & "小姐"
        Case "正式"
            MsgBox Left(Name, 1) & "女士"
        Case "撩"
            MsgBox "小姐姐"
        Case Else
            MsgBox Name
    End Select
End Sub

Private Sub Class_Initialize()
    Situation = "一般"
End Sub

Public Property Get Name() As String
    Name = mstrName
End Property
Public Property Let Name(lstrName As String)
    If Len(lstrName) <= 4 Then
        mstrName = lstrName
    Else
        MsgBox "姓名不能超过4个字符"
    End If
End Property

Public Property Get Gender() As pGender
    Gender = menumGender
End Property

Public Property Let Gender(lenumGender As pGender)
    Static blnFlag As Boolean
    If blnFlag = False Then
        blnFlag = True
        menumGender = lenumGender
    Else
        MsgBox "性别一旦设定,就无法修改"
    End If
End Property

Public Property Get Situation() As String
    Situation = mstrSituation
End Property
Public Property Let Situation(lstrSituation As String)
    mstrSituation = lstrSituation
End Property

Public Property Let DOB(ldatDOB As Date)
    Static blnFlag As Boolean
    If blnFlag = False Then
        blnFlag = True
        mdatDOB = ldatDOB
    End If
End Property
Public Property Get DOBtoString() As String
    DOBtoString = Format(mdatDOB, "YYYY年mm月dd日")
End Property

Public Property Get Age() As Integer
    Age = DateDiff("yyyy", mdatDOB, Date)
End Property

'模块1 测试代码
Option Compare Database
Option Explicit

Sub test()
    Dim objperson As Person
    Set objperson = New Person
    
    objperson.Name = "赵冰冰"
    objperson.Gender = Female
    objperson.Speak

    MsgBox "修改前性别:" & objperson.Gender
    objperson.Gender = Male
    MsgBox "修改后性别:" & objperson.Gender
    
    objperson.DOB = #2/3/1995#
    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
    Debug.Print objperson.Age               '立即窗口值:23
    objperson.DOB = #6/10/1995#
    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
End Sub

 

VBA 类模块系列之八——完善我们的第一个类

既然用Property Get / Property Let定义类的属性,我们能获得更多的灵活性,而目前我们的 … 继续阅读“VBA 类模块系列之八——完善我们的第一个类”

既然用Property Get / Property Let定义类的属性,我们能获得更多的灵活性,而目前我们的第一个类只有性别(Gender)属性使用了这种定义方式,本篇,我们将完善我们的类的属性,同时给大家揭示更多关于类的属性方面更深层次的问题。

姓名(Name)属性

我们将它的声明从公有(Public)改为私有(Private),同时使用变量命名规则将Name改名为mstrName。

Private mstrName as String

有了这样的一个私有模块级变量后,我们就有了一个存储属性数据的容器。接下来,我们要定义姓名(Name)的读取属性(Property Get)

Public Property Get Name() As String
    Name = mstrName
End Property

细心的读者会问,这里的属性名为什么使用的是’Name’而不是’mstrName’?属性名不用考虑命名规则了吗?这是一个很好的问题。有2方面的原因,我们必须要这么做:首先,类的设计者和使用者,可能会使用不同的命名规则。属性作为类的接口之一,我们(类的设计者)采用最一般的属性命名,是为了让类的使用者一看就懂。同时我们也隐藏了该接口的实现细节。其次,如果属性名与保存属性数据的私有变量名相同的话,会产生”二义性名称”的编译错误。

接下来我们为姓名(Name)定义赋值属性(Property Let),设计之前,我们要考虑下,是否要对姓名赋值的时候,做任何约束。比如,是否要检查一下赋的值的有效性?一般中国人的姓名都在4个字以下,对超过4个字的情况,是否要弹出错误提示框?另外,姓名被赋值一次后,是否允许再被第二次赋值?这些都是在设计之前要考虑的问题。这里我们就只检查一下超过4个字的情况吧。

Public Property Let Name(lstrName As String)
    If Len(lstrName) <= 4 Then
        mstrName = lstrName
    Else
        MsgBox "姓名不能超过4个字符"
    End If
End Property

添加一个出生日期(Day Of Birth)属性

为了让我们的类更加接近现实,我们给它添加另外一个日期类的属性:出生日期(Day Of Birth)。我们在类的头部,定义如下

Private mdatDOB As Date

前缀“mdat”中“m”还是模块级(Module)变量的意思,“dat”是日期类型(Date)的缩写。接下来,考虑给出生日期定义赋值属性,这里,我们假定要实施真正的“只赋值一次”。我们不能依赖mdatDOB的值去判断是否已经赋过值了,因为我们可以赋一个零值。在实际项目中,我们要使用布尔(Boolean)变量作为是否赋过值得标志(Flag)。出生日期的赋值属性定义如下

Public Property Let DOB(ldatDOB As Date)
    Static blnFlag As Boolean
    If blnFlag = False Then
        blnFlag = True
        mdatDOB = ldatDOB
    End If
End Property

我们使用静态(Static)布尔变量blnFlag(“bln”是Boolean的缩写)作为一个开关,所有布尔变量的初始值都为否(False),所以第一次给出生日期赋值的时候,blnFlag为False,判断条件blnFlag=False获得通过,紧接着将这个开关的值设置为True,然后将新的出生日期赋值给模块级的私有变量mdatDOB。第二次给出生日期复制的时候,静态变量blnFlag的值还是True,判断条件blnFlag=False就不再能通过了。这样就彻底的保证了出生日期属性只能写一次。这里必须使用静态变量,因为一般的变量在赋值子程序执行完毕后,值就被丢弃了。静态变量能将值一直保留在内存中,直到整个类对象被释放为止。

出生日期的赋值属性搞定以后,我们来考虑一下它的读取属性。我们读取一个日期,很多情况下,都是需要把它显示出来,日期的显示有很多的格式,例如2018年7月22日,2018年07月22日,2018-7-2,2018/7/2,2018/07/02等等。我们可以在读取属性的同时将它的显示格式也确定下来。我们用字符串类型来返回读取属性。代码如下

Public Property Get DOB() As String
    DOB = Format(mdatDOB, "YYYY年mm月dd日")
End Property

写完这3行代码后,我们可以编译一下,你会发现编译报错了!

问题出在哪里?

这里让我们先思考一下DOB是什么?显然,通过出生日期的赋值属性,DOB是可以出现在赋值号(=)左边的,赋值号右边的内容,将作为参数传递给赋值属性子程序。这个参数的类型是日期类型(ldatDOB As Date)。而通过出生日期的读取属性,DOB是可以出现在赋值号(=)的右边的,它的类型是字符型(String)。同样都是DOB,在赋值的之后,右边是日期类型,在读取的时候,右边是字符型,这就是一个问题!所以编译器报错:相同属性的定义不一致!

那怎么规避这个问题?非常简单,我们用另外一个属性名称就好了。我们将出生日期的读取属性做如下更改

Public Property Get DOBtoString() As String
    DOBtoString = Format(mdatDOB, "YYYY年mm月dd日")
End Property

这样,DOB和DOBtoString就是2个不同名字的属性了,尽管它们都是围绕着类的私有变量mdatDOB来读写。同时,我们还要注意,DOB只定义成赋值属性,只能出现在赋值号(=)左边,否则的话,编译报错:属性的无效使用。DOBtoString之定义成了读取属性,只能出现在赋值号(=)右边,否则的话,编译报错:不能给只读属性赋值。我们也可以将DOB理解为只写属性,DOBtoString理解为只读属性

年龄(Age)属性

有了出生日期,很自然的你会联想到计算年龄。我们何不添加一个年龄属性呢?当然这个肯定是一个只读属性了。我们定义年龄属性如下

Public Property Get Age() As Integer
    Age = DateDiff("yyyy", mdatDOB, Date)
End Property

从以上谈到的内容,我们应该能够获得一个这样的印象:类中的模块级私有变量与类的属性之间,可以不用存在一一对应的关系。例如mdatDOB这个模块级的私有变量,可以既可以和DOB/DOBtoString对应,还能和Age对应起来。如果拿Access中的表对象和查询对象来类比的话,mdatDOB好比表对象,DOB/DOBtoString/Age属性好比基于表对象的查询,表对象任何时候都不允许用户直接读写,用户必须通过查询来读写数据。这就是类中属性的定义与属性的实现之间的核心观点

最后将最终代码贴给大家:

类代码

'Person 类代码
Option Compare Database
Option Explicit

Private mstrName As String
Private mstrGender As String
Private mstrSituation As String
Private mdatDOB As Date

Public Sub Speak()
    Select Case Situation
        Case "一般"
            MsgBox Left(Name, 1) & "小姐"
        Case "正式"
            MsgBox Left(Name, 1) & "女士"
        Case "撩"
            MsgBox "小姐姐"
        Case Else
            MsgBox Name
    End Select
End Sub

Private Sub Class_Initialize()
    Situation = "一般"
End Sub

Public Property Get Name() As String
    Name = mstrName
End Property
Public Property Let Name(lstrName As String)
    If Len(lstrName) <= 4 Then
        mstrName = lstrName
    Else
        MsgBox "姓名不能超过4个字符"
    End If
End Property

Public Property Get Gender() As String
    Gender = mstrGender
End Property

Public Property Let Gender(lstrGender As String)
    If mstrGender = "" Then
        mstrGender = lstrGender
    Else
        MsgBox "性别一旦设定,就无法修改"
    End If
End Property

Public Property Get Situation() As String
    Situation = mstrSituation
End Property
Public Property Let Situation(lstrSituation As String)
    mstrSituation = lstrSituation
End Property

Public Property Let DOB(ldatDOB As Date)
    Static blnFlag As Boolean
    If blnFlag = False Then
        blnFlag = True
        mdatDOB = ldatDOB
    End If
End Property
Public Property Get DOBtoString() As String
    DOBtoString = Format(mdatDOB, "YYYY年mm月dd日")
End Property

Public Property Get Age() As Integer
    Age = DateDiff("yyyy", mdatDOB, Date)
End Property

类的测试代码:

'模块1 测试代码
Option Compare Database
Option Explicit

Sub test()
    Dim objperson As Person
    Set objperson = New Person
    
    objperson.Name = "赵冰冰"
    objperson.Gender = "女"
'    objperson.Speak
'
'    MsgBox "修改前性别:" & objperson.Gender
'    objperson.Gender = "男"
'    MsgBox "修改后性别:" & objperson.Gender
    
    objperson.DOB = #2/3/1995#
    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
    Debug.Print objperson.Age               '立即窗口值:23
    objperson.DOB = #6/10/1995#
    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
End Sub

 

VBA 类模块系列之七——重新设计类的属性

为了阻止类的外部使用者(比如模块1中的代码)随意来修改Person类的性别(Gender)属性,我们要学会使用 … 继续阅读“VBA 类模块系列之七——重新设计类的属性”

为了阻止类的外部使用者(比如模块1中的代码)随意来修改Person类的性别(Gender)属性,我们要学会使用另外一种定义类的属性(Property)的办法。

首先,我们将Public Gender As String 改为Private mstrGender As String,这样性别属性就被保护起来,外部使用者无法直接访问了。(你可能会问:“mstr”是什么?这其实就是程序员在定义变量的时候,约定的一个习俗。“m” 代表模块级(Module)变量,“str”是“String”的缩写。这样,当mstrGender这个变量出现在代码的其他地方时,我们一看前缀“mstr”就知道这是一个模块级的字符型变量。前几篇为了简化,没有变量命名规则的考虑,从本篇开始,我们会慢慢规范起来)

现在类的外部使用者再也不能直接访问修改mstrGender变量了,但是我们还是希望外部访问者能获取性别方面的信息,那该怎么弄呢?我们需要学习类模块中的两个新技能:Public Property Get / Public Property Let (还有一个类似的技能 Public Property Set 会在将来介绍)

Public是公有的意思,有了它,类的外部使用者才能访问。Property是属性的意思。Get是获取,Let是赋值。所以 Public Property Get是获取类的某个属性,Public Property Let是给某个属性赋值。我们修改Person类的代码如下:

Option Compare Database
Option Explicit

Public Name As String
Private mstrGender As String
Public Situation As String

Public Sub Speak()
    Select Case Situation
        Case "一般"
            MsgBox Left(Name, 1) & "小姐"
        Case "正式"
            MsgBox Left(Name, 1) & "女士"
        Case "撩"
            MsgBox "小姐姐"
        Case Else
            MsgBox Name
    End Select
End Sub

Private Sub Class_Initialize()
    Situation = "一般"
End Sub

Public Property Get Gender() As String
    Gender = mstrGender
End Property

Public Property Let Gender(lstrGender As String)
    If mstrGender = "" Then
        mstrGender = lstrGender
    Else
        MsgBox "性别一旦设定,就无法修改"
    End If
End Property

25-27行的代码,为类定义了一个获取性别(Gender)的属性。我们直接将类的私有变量mstrGender的值直接返回给访问者。

29-35行的代码,为类定义了一个设置性别(Gender)值的属性。(参数“lstr”中的“l”代表List,参数列表的意思)当你给性别赋值的时候,首先会检查mstrGender的值是否为空字符串,如果是,就直接赋值。如果不是,说明之前已经被赋值过了,就弹出对话框提示你不能修改。(实际项目中,一般不会用对话框来提示类的使用者,而是通过触发一个事件的方式来与类的使用者做信息的传递。)这样,性别属性就不再让类的外部使用者随意修改了。你只能为性别赋值一次,你可以叫这种属性为“只写一次属性”。

我们来看看类的使用效果,在模块1中,我们修改代码如下:

Option Compare Database
Option Explicit

Sub test()
    Dim objperson As Person
    Set objperson = New Person
    
    objperson.Name = "赵冰冰"
    objperson.Gender = "女"
    objperson.Speak
    
    MsgBox "修改前性别:" & objperson.Gender
    objperson.Gender = "男"
    MsgBox "修改后性别:" & objperson.Gender
End Sub

第12行和14行代码都是获取性别属性,第13行代码是对性别属性赋值。我们运行一下该代码,会依次弹出4个对话框

通过这种方式来定义类的属性,我们既能够保护类的私有变量不受到外部使用者的随意修改,也能够提供在我们允许的情况下访问和修改类属性的途径。

作为一个有经验的Access VBA开发人员,如果你掌握了这种定义类属性的方式,你可以开更多的脑洞,比如,如果在类中,将Public Property Get去掉,只保留Public Property Let会怎么样?或者反过来,将Public Property Let去掉,只保留Public Property Get又会怎么样?另外,29-35行代码,严格来讲,不能称之为“只写一次属性”,如果你第一次将性别赋值为空字符串””,那么你第二次还能再赋值。如果要设计一个真正的“只写一次属性”,你该怎么做?这些问题先留给有钻研精神的你思考吧。