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这种代码你应该是第一次看到,还不够你装逼的?^_^

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