VBA 类模块系列之十——类对象的生与死

在谈类对象的生死问题之前,我们先谈一下一般变量的生死问题。来看下面的代码: Sub test2() Dim a … 继续阅读“VBA 类模块系列之十——类对象的生与死”

在谈类对象的生死问题之前,我们先谈一下一般变量的生死问题。来看下面的代码:

Sub test2()
    Dim a As Integer
    a = 100
    Debug.Print a
End Sub

这段代码声明了一个整型变量 a ,这个变量 a 就在计算机的内存中,占据了几个字节的空间,然后被赋值为100,然后在立即窗口中被打印出来。最后,变量 a 离开了它的作用域,它所占用的内存空间被系统自动回收。

任何程序的运行都需要占用一定的内存,内存是一种有限的资源,所以不能随意浪费。占用完,必须要返还给系统,以备其他程序使用。

对于一般变量来讲,我们不用考虑它占用的内存的释放问题,因为VB/VBA没有指针的概念(严格来讲,VB/VBA还是有指针的,只是非常少用,不像C语言指针那么普遍,以后有机会再给大家介绍),所以不存在其他变量也指向变量 a 所指向的内存地址。如果一段内存地址永远只被一个变量所指向,那事情就非常好办了,该变量何时离开它的作用域,它所指向的内存就何时被系统释放回收。所以对于一般变量,我们完全不用考虑其占用内存的释放问题。

但是对于类对象变量来讲,情况就稍微复杂一点了。我们来看下面的代码:

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

类对象objPerson从什么时候开始占据内存空间的?这个内存空间又是何时返还给系统的?我们把类对象开始占据内存空间看作是类对象的生,内存空间被释放看作是类对象的死

我们在第6篇,类对象的初始化的时候,谈到了类与生俱来的2个事件:实例化事件(Class_Initialize)销毁事件(Class_Terminate)。这2个事件,刚好对应着类对象的生与死。

类对象开始占据内存空间的时候,实例化事件就被触发,实例化事件子程序(如果你为实例化事件写了事件代码的话)中的代码就会自动执行。实例化事件子程序也是初始化类对象的最好的地方。

相反,销毁事件,是在最后一个指向该内存空间的对象变量被置为Nothing的时候,被触发。在销毁事件子程序中,我们主要是做一些对象销毁前的善后工作,比如释放类对象内部的一些资源等,这个要等到创建多个类之间的关系的时候才会谈到。

为了清楚的给大家演示类的出生和死亡,我们可以分别在实例化事件子程序和销毁事件子程序中,添加一条简单的语句:Debug.Print “我出生了!” 和 Debug.Print “我挂了!”,一旦这2个事件被触发,你在立即窗口就能清楚看到。我们修改Person类代码如下:

'
'以上代码省略
'
Private Sub Class_Initialize()
    Debug.Print "我出生了!"
    Situation = "一般"
End Sub

Private Sub Class_Terminate()
    Debug.Print "我挂了!"
End Sub
'
'以下代码省略
'

我们使用F8快捷键逐语句执行 Test3 中的代码,你就能清楚的知道,当你执行Set objperson = New Person的时候,类对象的实例化事件被触发。当你执行End Sub的时候,类对象的销毁事件被触发。

为何我们不使用Set objPerson=Nothing,也能让类对象的销毁事件被触法?因为在Test3中定义的objPerson在执行End Sub的时候,已经离开了objPerson的作用域,所以objPerson被自动置为了Nothing,而类对象只有一个对象变量objPerson指向它,当objPerson被置为Nothing的时候,类对象的销毁事件就被触发,执行其中的代码,立即窗口就出现“我挂了!”

我们在做面向对象编程的时候,为了安全起见,一般不要去依赖对象变量的作用域来自动销毁类对象。而是显式的写一条释放语句:Set objPerson = Nothing。我相信你在使用ADO对象的时候,写过很多Set rst = Nothing的语句。

本篇要讲的内容基本上谈完了,但是我回头看了一遍,除了一点概念,没有什么值得装逼的地方,这不符合我的一贯作风^_^。所以,下面教大家一点装逼的技能。前方高能!

我在Access 开发框架(翻译+改编)系列之三——集合、类和垃圾回收中有谈到过对象编程的一条公理:一个对象会一直驻留在内存中,直到最后一个指向它的对象变量被设置成 Nothing。类对象变量与一般的变量最大的不同,就在于:可以有多个类对象变量指向同一个内存空间。

既然这样,我们弄2个对象变量,让它们指向同一个类对象,然后我们释放其中一个对象变量,看看会发生什么情况。编写测试代码test4如下:

Sub Test4()
    Dim objPerson1 As Person
    Dim objPerson2 As Person
    
    Set objPerson1 = New Person
    Set objPerson2 = objPerson1
    Debug.Print objPerson1 Is objPerson2
    Set objPerson1 = Nothing
    Stop
    Set objPerson2 = Nothing
End Sub

F8逐语句来运行Test4中的代码,看看类对象何时被创建,何时被销毁吧。装逼点在哪里?瞪大眼睛看第7行代码中的“Is”操作符!你应该写过 if obj is nothing then 之类的语句,obj is obj这种代码你应该是第一次看到,还不够你装逼的?^_^

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

'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()
    Debug.Print "我出生了!"
    Situation = "一般"
End Sub

Private Sub Class_Terminate()
    Debug.Print "我挂了!"
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

Sub test2()
    Dim a As Integer
    a = 100
    Debug.Print a
End Sub

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

Sub Test4()
    Dim objPerson1 As Person
    Dim objPerson2 As Person
    
    Set objPerson1 = New Person
    Set objPerson2 = objPerson1
    Debug.Print objPerson1 Is objPerson2
    Set objPerson1 = Nothing
    Stop
    Set objPerson2 = Nothing
End Sub

 

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