VBA 类模块系列之十一——类代码中的警察(二)

在《系列之九——类中的常量》中,我们为类定义了枚举类型的性别常量

然后在测试代码中为性别属性赋值的时候,IDE的智能提示跑了出来:

我当时拍着胸脯说,这回,类的使用者不会瞎给性别属性赋值了。好吧,我承认当时有点说大话了。因为当时闻到空气中兴奋的气味,所以不想扫大家的兴。

我们弄个极端的例子:某泰国的lady boy在给这个属性赋值的时候,一看提示出来的2个选项,没有一个是他(or 她?)能选的。心想,MD,这不是明摆的歧视我这个第3性嘛,我就给赋个值3,然后编译一下,居然编译通过了!

为什么能编译通过?还记得我说过,枚举常量用起来是非常简单的,简单到什么程度?不负责任的程度^_^。枚举类型,从本质上讲,是长整形(Long),你拿任何一个长整形的值赋给它,它都无条件接受。至于你在这个枚举类型下面,定义了多少个常量,它一概不管。

好吧,既然你知道了类中枚举常量的底线轻易的就可以被人突破,导致类的属性,被设置了非法的值,你说怎么办?既然是非法,当然得交给警察来管!

我们终于切入正题了。

我们将原来性别赋值属性子程序(Public Property Let Gender)中的代码全部删掉(相信你已经掌握了如何设计只赋值一次的属性),在其中按照上一篇的样式设置警察,警察局如下面代码所示:

  • 第2行,第8-14行,就是我们上一篇介绍的内容,不再赘述。
  • 第3行代码,很容易理解,如果属性被赋的值,既不是男,又不是女,那就是非法的值了!
  • 第5-7行,是合法的赋值。

现在问题就来了,第4行我们要写什么代码,才能让警察现身?上一篇是故意用一个0作为除数,本篇还要这么搞一下么?这样固然可以把警察召唤出来,但是有点为了目的,不择手段的感觉,咱们可都是有品味的人^_^。

我们这里的困境是,属性被赋了非法的值以后,怎么才能让警察知道?等等,什么是非法?这个法律是谁说了算?是你规定了性别只能是男和女,当然是你说了算。既然别人给性别赋了一个既不是男,又不是女的值,你就得跳出来说,这是非法的!而不是自己做一件非法的事(比如弄个0做为除数),以便故意把警察引出来。所以现在的问题是:你通过什么指控别人非法?

每当你穷尽脑汁还是一筹莫展的时候,就是学习新东西的时候了。话说,你对 “Err” 这个系统对象,了解么?作为一个有一定经验的VBA开发人员,你使用这个系统对象,多半都是在错误处理程序当中,当发生运行时错误了,你一般会去调用 Err.Description 来看看错误说明。这个对象还能用来指控别人非法!怎么指控?用Err对象的Raise方法(raise可以翻译理解为提起诉讼的意思)。

Err.Raise的第一个参数是错误代码(Number),可以理解为违反的是第几条法律。不要忘记了,性别必须是男或者女,这法律可是你定的,我C,有没有一种很爽的感觉?^_^,虽然这条法律是你说了算,但是你在给它编号的时候,要注意不要使用系统中已经被占用的错误编号。我们修改第4行代码如下:

第4行代码,给错误编号为vbObjectError+513,为什么弄这么一个奇怪的编号?因为vbObjectError+512之前的号码都被占用了。vbObjectError是一个系统常量。另外,我们设置了错误的来源(Source)和错误的描述(Description)。

代码运行到第4行,被宣布违反了你制定的法律以后,警察就把你拷起来带到了警察局,第13行。

问题又来了,在警察局你要做什么事?上一篇是用MsgBox检讨了错误,就给放了。这里你要做同样的事情吗?当然,这个决定权在你。你可以MsgBox弹出来告诉用户,它违反了你的什么法律,你还能在这里将这件事记下来,写入数据库中,留下永久的案底。你想怎样都可以,因为在编程的世界里,你是上帝。^_^

对于现在的情况,我的建议是,将错误上报给上级机关,然后,然后就没有然后了。这样有什么好处吗?呃,省事,我只负责立法和司法,确保我设计的类中没有违法乱纪的行为。行政处罚就交给使用我的类对象的上级主管单位吧,是做检讨撤案,还是写入档案,由类的使用者来决定。

我们修改第13行代码如下:

第13行我们还是用的Err.Raise来向调用这个子程序的程序提起违法诉讼。

现在我们整体梳理一遍这个警察故事二:首先我们设了一个警察和警察局,然后当性别被赋予违法的值的时候,我们提起了指控,代码被带到警察局,警察局直接将指控上报给了上级机关。

发现问题没有?如果警察只是把罪犯抓起来送给上级,和没有警察是一样的。因为上一篇讲过,如果你在县里不设置警察,代码在县里发生的运行时错误,会自动上报至市里的警察。所以我们可以把县里的所有警察和警察局都撤掉,代码如下:

世界一下子清净了不少^_^

我们修改一下类的测试代码如下:

F8逐步执行一下就会知道,虽然你将性别赋值为3,编译器没有检测出非法代码,运行的时候还是没逃出法律的制裁。

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

 

VBA 类模块系列之十一——类代码中的警察

好吧,我承认本篇的标题有点哗众取宠,不过这是我能想到的最佳的类比方式——以你最容易理解的方式——把我想谈的讲清楚。类代码中的警察就是类代码中的错误处理

我们先来谈谈标准模块或者窗体模块中的错误处理。

作为一个有一定VBA编程经验的你,应该写过错误处理代码。我们来举个例子,在Access中新建一个空白的窗体,然后拖一个按钮到这个窗体上,窗体和按钮的名字都用默认的就好了,保存以后进入到IDE界面,为按钮的单击事件写一段简单的代码如下:

这个子程序除去错误处理代码以后,非常简单,声明了2个变量,给变量a赋值为1,然后用1除以0的值,赋给变量b。大概没有人能写出比这还蠢的代码了。^_^ 之所以要这么干,就是故意让警察抓个现行。

警察在哪呢?第6行代码就是!第6行代码翻译成大白话就是:如果在接下来的代码(从第7行开始)中发现了违法的代码,就铐起来带到名叫“ErrHandler”警察局,“ErrHandler”警察局在哪里?第15行。为什么警察局的名字叫“ErrHandler”?呃,这个名字的意思是“错误处理”,其实取什么名字是你说了算,我一般取这个名字的警察局,你可以换成你喜欢的名字。

第10行代码是一个非常典型的运行时违法代码,或者叫运行时出错代码,计算机无法将0作为除数计算一个结果给你。

  • 为什么我这里要加一个“运行时”?因为第10行代码在编译的时候,不会报错,只有在运行的时候才会。
  • 为什么在编译的时候不会报错?呃,你可以把0想象成一个参数,编译的时候,这个参数的值还没有确定,只有在代码运行的时候,发现这个参数是0,然后作为除数,参与计算,就报错了。
  • 为什么我们不考虑编译时的违法代码?因为如果编译的时候发现了违法代码,比如使用了未定义的变量名,编译器会报错,你不去修改这些错误代码,你连运行都无法运行。
  • 还有别的运行时出错代码吗?呃,有的,多了去了。比如代码去打开一个不存在的文件,数组变量越界访问等等。

好了,关于运行时出错代码的十万个为什么到此结束,假定你都理解了。我们从头来捋一遍。

  • 我们放置了一个警察在第6行代码中,告诉它,如果发现运行时的出错代码,就铐起来带到“ErrHandler”警察局。
  • 当程序运行到第10行的时候,代码运行出错了
  • 所以程序就从第10行跳到了15行,警察局的位置,然后接着往下执行。
  • MsgBox向你检讨错误编号,错误描述,错误来源。
  • 接下来执行第21行代码,Resume ExitProcedure,这句代码的意思翻译过来就是说:好了,警察局问话结束,该交代的都交代了,撤销你的案底(Resume),然后去ExitProcedure报到把。
  • 然后程序跳到第11行接着往下执行
  • 第12行代码与第6行代码很像,也是放置了一个警察,但是这回不是让它铐起来带回警察局了,而是直接撤销案底(Resume),接着往下执行(Next)。然后如果在执行到第14行之前,遇到任何执行错误,通通就地无罪释放。
  • 最后到第14行,结束程序的执行。

我感觉我刚刚写了一篇侦探小说。^_^ 本来只想简单介绍一下背景的,结果一发不可收拾。。。。。。本篇就作为警察故事一吧。

既然想好了写个警察故事二,那在这个警察故事一中,在加点料。

关于错误处理代码,除了刚刚介绍的那些意外,还有一个也很重要的代码:

On Error GoTo 0

如果让人工智能来推断这句代码的意思,应该就是:设置一个警察,一旦发现运行时错误,就把它拷起来带到叫做0的警察局。哈哈,所以说人工智能还是很蠢的。这句代码真正的含义是:从这句代码开始,把警察撤走。

什么?把警察撤走了?那出了运行时错误,谁管?把一个小县城的警察局撤销了,这个小县城的违法犯罪你说归谁管?当然是上一级单位,市警察局管了。这就相当于,你的子程序代码中没有任何错误处理程序,如果运行时出错,就归调用你的子程序的程序(上级单位)的警察管了。

最后再提出一个问题,如果在警察局里面发生了运行时错误,会怎么样?你可以自己写代码验证一下你的直觉。^_^

好吧,希望你对错误处理代码有更深刻的理解,这样在类中的错误处理代码才更容易掌握。

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

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

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

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

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

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

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

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

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

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

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

我们使用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如下:

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

姓名(Name)属性

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

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

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

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

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

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

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

我们使用静态(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等等。我们可以在读取属性的同时将它的显示格式也确定下来。我们用字符串类型来返回读取属性。代码如下

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

问题出在哪里?

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

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

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

年龄(Age)属性

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

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

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

类代码

类的测试代码:

 

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类的代码如下:

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

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

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

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

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

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

VBA 类模块系列之六——类对象的初始化

上篇谈到类对象在初始化的时候,某些属性要是能够自动初始化就好了,会省去你一些麻烦。这就是本篇要谈的,类对象的初始化。

我们知道类的接口包含了属性,方法和事件。这里的事件是指我们在类中定义的事件。类除了我们外加给它的自定义事件以外,它与生俱来的就有2个事件:实例化事件(Class_Initialize)销毁事件(Class_Terminate)。现在只谈实例化事件。

实例化事件,或者叫初始化事件,顾名思义,是在类实例化的时候自动触发。类什么时候被实例化呢?就在执行set objPerson = New Person这条语句的时候。如果你在初始化事件中写入一些代码,这些代码就会自动执行。类的初始化事件是我们初始化类属性的最佳场合。

我们在Person类代码模块中,代码窗格左上方的对象下拉框里面选择Class,右上方的事件下拉框中选择Initialize,类的实例化事件的响应子程序就自动创建好了。

Class_Initalize()这个事件响应子程序,与大家比较熟悉的Form_Load()事件响应子程序没有什么不同。只要这个事件发生了,那么就会自动执行其中的代码。现在我们就可以把场合(Situation)这个属性的初始化放在Class_Initalize()中了。

既然场合(Situation)属性已经被初始化了,标准模块中就不用再初始化该属性了。

我们重新执行一遍Test()子程序,会得到同样的结果。

我们知道,一个人生下来是男是女,这辈子都不会变了,当然你说可以到泰国去变性,好吧,你说得对,但一般大多数人是不会这么干的。我们来看看我们的类,你会发现,性别初始化以后,我们很容易的就能将它修改了。我们在Test()子程序中,可以随意的添加一行代码:objPerson.Gender = “男”,这个人就变性了。

在类中,我们有没有办法阻止类的使用者做这样操作呢?答案当然是肯定的^_^下一篇将给大家介绍一种更加健壮的定义属性的方法。

VBA 类模块系列之五——使用你的第一个类

第二篇中我介绍过,类就是做饺子的塑料模具。到目前为止,我们做好了一个叫做Person的模具。现在我们要用这个模具做真正的饺子了。

还记得你是怎样使用ADODB库中的Recordset对象的吗?你在窗体代码模块,或者标准代码模块里面声明一个Recordset变量(Dim rst as Recordset),然后使用Set rst = New Recordset 语句创建一个Recordset对象,将rst指向这个新创建的对象。这里我们也是一样的操作:插入一个标准代码模块,然后添加一个子程序Test(),在子程序中添加如下代码:

现在对象变量objPerson指向了一个真正的“饺子”了,这个“饺子”有3个属性:姓名,性别,场合。现在这3个属性都是空白的,什么值都没有。所以我们要先给它们赋值。

属性赋值以后,我们就可以调用类的方法了,目前只有一个方法Speak。

我们执行一下这个子程序,一个这样的信息窗口就会跳出来:

我们成功了!感觉如何?我们创建了自己的类,又用它实例化了一个对象,设置了它的属性,调用了它的方法。

欣喜之余,冷静的读者可能会提出一个问题来:调用类的方法Speak前,还需要设置这么多的属性,忙乎了半天,这个类也没帮到我们多少事情,何必呢?好吧,你说得对。如果对象在创建的时候,某些属性就能自动初始化,比如让场合(Situation)这个属性默认设置为“一般”,那就好了。我们就可以少初始化一个属性了。

这个问题当然是有办法解决的。这正是下一篇我们要谈到的:类的实例化事件。

VBA 类模块系列之四——创建你的第一个类

前三篇终于啰嗦完了,牛逼也吹完了,我们现在来创建一个类。

  1. 首先在VBE里面,选择插入菜单-类模块,一个空白的类模块就创建好了。

2. 接下来,要想一下,我们用这个类来干嘛用?还没想好去哪里,我就出门了,哈哈。这个应该放在第一步。好吧,比方说我们现在要用这个来为模拟一个人(Person),第二篇里面谈到,类的接口是由属性方法事件构成的,那么我们要模拟人的哪些属性,方法,事件呢?

我们先来考虑属性(Properties)

人的属性太多了,姓名,性别,年龄,身高,体重,民族等等。我们弄简单点,只考虑他的姓名(Name)和性别(Gender)。

我们确定了目标,就可以开始行动了。因为我们要用这个类来模拟一个人(Person),那首先我们就将这个类的名字改成Person。这个很简单,在左下角的名称属性里面,将“类1”改为“Person”。然后我们在类中建立姓名和性别这2个属性。

为类创建一个属性,最简单的方法就是在类的头部声明一个公共变量。我们申请2个:

3. 属性添加好了,接下来考虑方法。这个类需要什么方法呢?因为这个类就只有2个属性,姓名和性别,比如某个美女,她叫赵冰冰,你会怎么称呼她呢?这个得看你跟她的关系了。如果你们之间很陌生的话,你会叫她“赵小姐”,如果是在非常正式的场合,你应该叫“赵女士”,如果你想撩她,你会叫她“小姐姐”。我们给这个类添加一个名叫称呼(Speak)的方法。但是根据什么来确定你称呼的方式呢?我们需要再多添加一个确定场合(Situation)的属性,有了这个属性,我们才好确定该用何种方式称呼她。

我们添加好了如上的代码,我们只分了3种场合,如果不是这3种场合,我们就直接称呼她的姓名好了。

我们的第一个类就这样轻松设计好了。

VBA 类模块系列之三——为什么要使用类

本篇我只想说一件事实:在国外的Access VBA开发者社区,会使用类是一件很酷的事情。大家去国外论坛验证一下就知道了。接下来的内容,吹牛逼的成分比较多,但是作为一个严肃的系列文章,该吹的牛逼还是要吹的:)。大家要是忙,可以直接跳到下一篇。

可以说,你在Access中的绝大多数编程任务都可以用标准模块来完成。那么你为什么要使用类模块呢?这个问题可以从以下几个角度来看。

数据和行为的封装

VBA的类模块能带给你的一个最大的好处,就是你可以将一些相关的数据和行为封装在这个类模块中。

我记得好几年前,我还在市场部做数据分析预测的时候,经常要查将来5年还是10年每年节假日的具体日期。元旦,五一,国庆这3个假期,最简单了,每年都是固定的日期。清明和中秋就比较麻烦了。中秋是按农历,每年都不一样,清明是按节气,虽然每年都差不多是4月4日或者5日,但是你拿不准到底是哪一日。为了一劳永逸的解决这个问题,我在网上找到了一个Excel VBA 类模块,里面有封装了天干,地支,节气,星座等等你能想得到的数据,还有很多函数,公有的函数提供对外的调用,私有的函数供类内部计算使用。有了这个类,你能将任何将来的日期转化为对应的农历日期。喜欢刨根究底的我,特意去研究了类中的代码,但是还是看不懂,不是代码看不懂,而是对农历历法一无所知,所以不知道代码为什么要这么写。我进一步去研究农历历法,结果我找到了一片国外的论文,看着从中文翻译成英文,又在我脑中转成中文,那种感觉特别别扭。从外国人对中国的农历历法的研究中,我得出一个结论:从地球绕太阳公转的角度来看,农历比公历更加准确。呃,这牛逼吹到民族自豪感上去了:)

一个类中,既有数据,又有方法,它就能实现一定的特定功能。难道这个就非得用类模块来实现吗?标准模块不行吗?这是个很好的问题,我们来看看如果是标准模块,怎么来实现。标准模块中,你可以声明一些私有的模块级常量和数组变量,将这些天干,地支,节气数据放在其中,然后私有和共有的函数或方法保持与类模块中的一致。传入参数调用某个公有函数,第一次调用的时候,模块及的变量当然是没有初始化的,需要检查一下是否初始化,若没有就初始化一下,然后执行公有函数的代码,也能得到一样的结果。你唯一不能做的,就是同时创建一个类的多个实例。标准模块不存在实例的说法,它一旦被调用,永远只在内存中保留一份私有模块级变量数据。所以你无法同时创建2个日期对象数据。一旦你遇到需要同时保留一份以上的日期数据的时候,你能做的,就是再创建一个一模一样的标准模块。这就回到了该系列第一篇里面谈到的,在多处维护相同代码的窘境。

将复杂的实现逻辑隐藏起来

这个好处是显而易见的。我并不需要事先知道农历历法是怎样计算的,就能将一个公历日期转换为农历日期。我只需要知道类的接口,知道如何调用它就好了。这种示例在Access中还有很多,比如ADODB库中的数据集Recordset类,天知道它是怎么将表中的数据读取到内存,还有一个游标说它目前指向的那一行数据,你执行一下MoveNext方法,它就指向到了下一行。我只需要知道这样操作,就能得到相应的结果,至于它背后是如何实现的,你可以心安理得的保持无知状态。因为这不是你需要操心的事情。正式因为类有这种独特的隔离特性,使得程序员与程序员之间的分工协作变得容易。一个大型的项目就是由很多程序员合作编出来的。在这里你也能感受到接口的重要性。还是那句话,一旦发布了的接口,就不能去修改了,但是可以添加新的接口。

让程序员之间的分工写作变得容易

上面已经谈到了,这里谈谈自己的经验。我使用Access一般都是做一些部门级的系统,说大不大,说小也不小,都是一个人开发,还未有与他人合作开发的情况。所以不存在分工的问题,尽管如此,我还是习惯将用户界面与数据读写相分离,中间用类来做2层之间的交互,同时还有一些其他的辅助类,实现一些特定的商业逻辑。当然你可以说这已经脱离了Access的快速开发的初衷,谁说不是呢?Access初学者习惯使用绑定窗体,但你知道的,用户的需求是很刁钻的,我举个例子,绑定窗体如何能保存数据更改的历史记录?在多用户环境中,你还需要记录下是谁在什么时间做的更改?是将哪个旧数据改为哪个新数据的?当然你可以在绑定窗体中,对每个绑定的控件的change事件编写代码,来实现,但万一修改又被撤销了呢?将业务逻辑代码散落在每个窗体里面,也不值得提倡。窗体最理想的状态,是只做用户交互。

这些个人风格上的事情,仁者见仁智者见智了。牛逼吹完了,欢迎拍砖:)