|
VB程式设计内功讲座(二 ) 变数(物件)在程式中的活动模式 有时候觉得写程式的工作跟侦探差不多,因为在程式的开发过程中,总免不了会有bug发生,而程式设计者的责任就是把制造 bug 的元凶找出来。别以为当个侦探很好玩,除了必须绞尽脑汁找出问题之外,还要做到勿枉勿纵,如果程式是一组人开发出来的,有问题的时候,每个人都不希望问题出在自己所写的程式中,身为一个组长,必须要有足够的能力判断可能是元凶的程式是哪一个,然後要求组员进行检测,以求正确地找出问题的原因,否则没有问题的程式检测了半天,不仅劳民伤财,还会延误程式开发的商机。 程式错误的原因很多,但根本道理却在资料(变数或物件)上面。我们可以把程式简单地分成「程式码」(code)及「资料」(data)两部分,虽然两者都可能是造成错误的原因,但程式码的部分在执行阶段是死的,比较容易侦测,而资料的部分则会随着程式执行的状况来改变,是比较难掌握的部分,笔者作个比喻,某一大楼发生了窃案,那麽侦察的方向有二:(1)检查保全系统是否有漏洞:由於保全系统是固定的,所以比较容易检查,如果与程式作个比较,它像是程式码的部分,(2)嫌疑犯的调查:对於可能进出大楼的人员进行调查,但由於人的行为是自由的,所以这个部分的调查工作要比保全系统的检查来得困难,如果与程式作个比较,它像是资料的部分。 上一期我们介绍了变数的组成元素,藉以了解资料的基本特性,本文让我们继续探讨资料在程式中的活动模式,藉以在程式出状况时能够抓出造成程式错误的元凶。
本文大纲
记得有一种活动,是在湖里抓鸭子,别小看鸭子笨笨的,水中的鸭子其实是很难抓的,所以比较常用的技巧是将鸭子赶到角落,以缩小鸭子的活动范围,最样子就比较容易抓到鸭子了。变数的道理也高深不到哪里去,但如果我们连变数的活动范围都搞不清楚,那麽就别想抓到因为变数所产生的错误。 区域变数 区域变数(local variable)顾名思义就是指在某一个区域里面活动的变数,当程式进入此一区域时,区域中的变数便会诞生,但是当程式执行过此一区域时,变数即告消失,典型的例子是程序(副程式、函数、或事件程序)中的变数,例如: Sub SubX() 以上面这个 SubX 副程式为例,每次呼叫进入时,变数x才会诞生,而当程式执行过 End Sub 叙述时 (也就是程式结束此一区域的执行时),变数x即告消失,因此每次当SubX 副程式再度被呼叫时,变数 x 的初值都等於 0。 区域变数由於活动范围仅局限於某一程序,因此只要这个程序不是写得太大,变数的变化情形就很容易掌握,也就很容易侦错了,这是程式设计中最常使用的变数类型。 全域变数 所谓全域变数,指的是所有区域皆可使用的变数,也就是说不受「程序」范围所限制的变数。在VB里面凡是撰写在程序之外的变数均属於全域变数,例如: Dim x As Integer ' 全域变数,可供多个程序共用 在VB里面,全域变数又可分成「模组私用」全域变数、「模组公用」全域变数、及「专案」全域变数,稍後笔者会有更一进步的说明。 静态变数 所谓静态变数,在VB里面是指利用 Static 保留字宣告的区域变数,例如: Sub SubX() 以上的静态变数 x 与区域变数 y 一样,其活动范围都限定於 SubX 副程式之中,但每次进入 SubX 副程式时变数 y 的值都会归 0,而x则保有其原来之数值,所以 SubX 副程式中的 Print x 每次列印的数值都会累加100。 选择变数类型的基本原则 当我们决定使用某个变数时,该将它宣告成区域、静态、还是全域变数呢?请参考以下的基本原则: 1.是否要提供给多个程序共用,如果是,才将变数宣告成全域变数。 有些人可能会觉得将变数宣告成全域变数最方便,因为任何程序都可以使用,但是全域变数的缺点是不容易侦错,以抓鸭子的活动为例,鸭子的活动范围越大,就越难抓到,同样的,任何程序都可以使用的变数,万一其变数值与我们预期的结果不一样时(这当然是bug),就必须逐一对每一个程序进行侦错,无形中增加了程式侦错的困难度。 2.是否必须记录程式执行的状态,如果是,则必须变数宣告成静态变数,因为区域变数每次进入程序的值都会归0,所以无法一直纪录着程式的执行状态。 3. 如果不属於情况1、 2, 则将变数宣告成区域变数。 Option Explicit:强制变数宣告 VB 允许我们在不必事先宣告变数之下,就使用变数,例如: Dim X As Integer ' 事先宣告变数X 但这样的程式撰写习惯却可能引起一些不容易侦测的错误,例如: For i = 1 to 10 以上程式中的 Arr(j) 应该是 Arr(i) 才对,但由於 VB 允许未宣告而直接使用变数,以致 j 被视为合法的变数,但j在回圈中却一直等於0。 在不必事先宣告变数之下就使用变数,就好像变数随时随地都会冒起来,跑到我们的程式中,而从以上的例子中,我们发现这种作法其实是有缺点的,为了改善这个缺点,VB提供给我们另一种选择,那就是在所有程序之外加上 Option Explicit 的叙述,加上 Option Explicit 的作用是:「要求 VB 把未事先宣告的变数视为错误」,因此以上程式若修改成: Option Explicit 则由於 j 变数未事先宣告,会被VB视为错误,因此可在编译阶段提早被侦察出来。 对VB而言,一个专案可以含有多个模组,如果我们在某一个模组之中宣告了全域变数,那麽这个全域变数可以提供给该模组的所有程序使用,是无庸置疑的事,但是在多模组的专案中,我们则要考虑另一个问题:某一个模组的全域变数可以给其他模组使用吗? 模组私用全域变数 在表单模组或一般模组中,如果我们利用Private或Dim 保留字来宣告全域变数,例如:(注 :一般模组指的是利用功能表的「专案/新增模组」而加入於专案之中的模组,此类模组将来会以.bas 的副档名来储存) Private x As Integer 则此一变数为「模组私用」全域变数,也就是说,此一全域变数只有该模组的程序可以使用,其他模组则不可以使用。 模组公用全域变数 在表单模组中,如果我们利用Public保留字来宣告全域变数,例如: Public x As Integer 则此一变数为「模组公用」全域变数,也就是说,此一全域变数也可以给其他模组使用。但请注意,使用的语法必须在变数之前冠上表单名称,假设以上例子中的全域变数宣告在 Form1 之中,则以下是Form1 模组中的程序与Form2 模组中的程序,在使用变数x上的差异: ' Form1 模组
' Form2 模组 由於Form2 模组使用的是 Form1 模组的全域变数,所以必须在变数之前冠上「Form1.」。 专案全域变数 在一般模组中,如果我们利用Public 保留字来宣告全域变数,则此一变数为「专案」全域变数。所谓专案全域变数,指的是同一专案中,所有模组的所有程序均可使用的变数,因此以这种方式所宣告的变数其活动范围将扩及整个专案。 最後我们以一个实例来整理以上各种变数在程式中的活动范围,假设专案中有含有两个表单模组— Form1、 Form2 及一个一般模组 Module1,而这几个模组中的所宣告的变数如下:
则这些变数在几个副程式中的可使用性如下表 :(「??」符号表示可使用、「F1」表示必须冠上「Form1.」才可以使用、「F2」表示必须冠上「Form2.」才可以使用,空白者表示不可使用)
物件的活动范围 物件按性质可分成控制元件、表单物件、及一般物件叁种,其中一般物件与变数一样,可分成区域物件、静态物件、及全域物件叁种,活动范围也与变数一样,以下让笔者来说明控制元件与表单的活动范围。 控制元件的活动范围与表单完全相同,当表单被载入时,表单上的控制元件即会被载入,当表单被载出时,控制元件即会被载出,若与变数做比较,则与全域变数的活动范围完全相同。 由於表单中全域(静态)变数及控制元件的诞生(灭亡)取决於表单的载入(载出),因此我们必须特别注意表单载入与载出的时机。导致表单被载入的叙述有「Load 表单名」、「表单 .Show」、及「使用表单的属性、全域变数、控制元件」叙述;至於表单被载出的情况则有「Unload 表单名」叙述及「使用者关闭表单」。 当表单被载出系统时,全域变数的变数值会归零,表单及控制元件的属性值会还原成设计阶段时的设定值,因此如果我们想保留变数值及属性值,不要使用「Unload表单名」关闭表单,须使用「表单名.Hide」或「Form.Visible = False」隐藏表单。但笔者必须特别提醒您一点,如果有表单被隐藏,则程式结束前应该使用以下方法载出所有的表单: For I = 0 To Forms.Count - 1 Unload Forms(I) Next 否则表面上所有的表单已经被关闭,让人误以为程式结束了,而实际上却还有表单在系统之中。 由於变数执行时会占用记忆体,因此如何正确地使用变数,也会影响系统的运作,首先让我们思考一个问题:「当程式越写越大时,程式所使用的变数也越来越多,会不会演变成程式执行所需之记忆体超过系统记忆体,而不能执行」,例如我们就经常看到某些产品在包装盒上面会标明「至少须 16MB 记忆体」之类的字眼。 需要使用大量记忆体的软体通常是功能强大的知名软体,我们自己写的程式是不是也会出现越来越吃记忆体的情事呢?感觉应该不会,但实际不然,如果使用变数时不了解系统内部的基本运作,则即使是小程式也可能会把系统记忆体吃光。 全域变数与静态变数 就我们所讨论的叁种变数而言,全域变数及静态变数会在所属之模组被载入系统时,占据一块记忆体,参考以下程式: Dim X(1024) As Integer ' X占有 2 KB 记忆体 Sub SubX() Static Y(2048) As Integer ' X占有 4 KB记忆体 End Sub 如果以上程式附属於「一般模组」(会在程式被载入时,即载入系统),则程式被载入系统时,X及Y阵列将占据6 KB 的记忆体,而直到程式被载出时,这6KB的记忆体才会归还给系统;如果以上程式附属於「表单模组」,则必须等到表单被载入时,X及Y阵列才会占据 6 KB 的记忆体,而当表单被载出时,这 6KB 的记忆体即会归还给系统。 了解以上的基本特性之後,笔者想特别说明以下两种通病: (1) 将所有的全域变数都放在「一般模组」中:的确,将共用的变数放在一般模组中最为简便,但由於一般模组中的变数会在程式被执行时即占用系统记忆体,而在程式结束时才归还记忆体,这使得变数在程式执行期间一定会占用记忆体,而减少了系统可运用之记忆体。要避免此一问题,宣告大型变数(例如 Dim Y(40960) As Integer会占用 80 KB)时,想一想被宣告的变数是不是只有某一「表单模组」需要使用,如果是,则将那些变数放在表单模组中,若是小型变数(例如 Dim X As Integer 只占用 2 bytes), 则可不必顾虑此一问题。 (2) 将其他模组可能使用的变数放在某一「表单模组」中,然後使用「Form名 .变数名」的格式来使用变数:此一方式并没有什麽错误,但请注意当表单被载出时,表单中所有变数的记忆体将会归还系统,而使得变数值全部归零,如果这些变数的用途是记录程式执行的状况,则在表单被载出时,将会失去记录的功效。 区域变数 当程式进入某一区域时,系统会为该区域的区域变数配置好记忆体,而程式执行过该区域时,变数所占用之记忆体又会归还给系统。 区域变数可能占用的记忆体有「堆叠」及「系统记忆体」两种,其中堆叠是程式载入时就配置给程式的(通常小於64KB),不管怎麽使用都不会影响系统记忆体,对於小型变数而言,使用的是堆叠中的记忆体,但如果是大型变数,则须占用系统记忆体。 由於区域变数所使用的记忆体在不使用时即归还系统,一般而言并不会出现耗尽系统记忆体的现象,须特别注意的是递回呼叫(例如副程式呼叫自己,或者副程式A呼叫副程式B,而副程式B又呼叫回副程式A),如果程式没有设计好递回呼叫的出口,则副程式会一直呼叫下去,则位於该副程式中的变数将会持续地占用记忆体,而可能用尽堆叠或系统记忆体。 物件与系统记忆体 物件与变数最大的差异如图-1: 图-1 变数与物件的比较 比较特别的是物件的「具体物件」部分,当我们宣告某一物件变数时(例如 Dim X As Object),具体物件还不存在,直到执行建立物件的叙述之後,物件才会含有具体物件(详阅上一期VB专栏)。 若撇开具体物件的部分,物件与变数在记忆体的使用上是完全相同的,所以我们可以把重心放在具体物件上面,具体物件所占用的记忆体通常会跟物件变数一起被归还给系统,例如: Sub SubX() … 则程式执行到 End Sub 时,X所占用的记忆体会被释放,而属於X的具体物件也会一并被释放。 但并非每一种物件的具体物件都具有以上特性,有些物件的作法是,如果程式要释放具体物件,一定要呼叫释放物件的方法,如果程式忘了这麽做,则即使程式结束执行了,该具体物件依然会占据系统记忆体,还好的是此类物件并不多见,笔者提出此一状况,只是想提醒您一点,当我们使用某一种新的物件的,须查阅该物件的说明文件以了解该物件是否为此类物件。 当我们将一个大程式分成多个模组或程序之後,为了让不同程序之间能够共享资料,除了全域变数的使用之外,另一个方法则是程序呼叫时的参数传递。诚如我们前面在「选择变数类型的基本原则」段落中的说明,全域变数少用为宜,因此参数的传递在程式设计中就益形重要。 在VB里面,程序之间的参数传递有两种方式:「传值呼叫」(call by value)及「传址呼叫」(call by address),这两种参数传递方式在表面上很容易让人忽略其差异性,但实际上却会影响程式执行的结果,以下先以一个例子来说明这个问题,假设有一副程式定义如下: Sub AddOne( x ) 而以下是两种不同的呼叫方式:
结果呼叫「Call AddOne(i)」之後,所得到的 i 值等於11,而呼叫「Call AddOne( (i) )」之後,所得到的i值却等於10。 注: 在以上的呼叫格式中,往往会省略 Call 保留字,如果省略 Call 保留字,则「Call AddOne(i)」要写成「AddOne i」(去掉参数周围的左右括弧),而「Call AddOne((i))」则写成「AddOne (i)」。 上述例子会有不同的输出结果,是因为参数传递方式有所不同的关系,以下让笔者来解说上述两种参数传递的差异及工作原理。 实际参数与形式参数 当我们定义副程式时,例如前面的 Sub AddOne( x ),由於我们并不知道将来呼叫者会传入哪些资料作为参数,因此只好给这个参数取一个暂时性的名称,例如x,而将来副程式被呼叫时它们会被真正的资料 (常数值或变数)所取代,所以这些参数都只是形式上的,故称之为「形式参数」(formal parameter)。 而呼叫程式端在呼叫副程式时,必须以实际的资料来替代形式上的参数,使得副程式能够拿到真正的资料来运算,这真正的资料就叫做「实际参数」(actual parameter),例如 Call AddOne( i )中的i。
所谓参数传递的方式,就是在呼叫副程式的过程中,编译器(或解译器)如何以实际参数来替代形式参数的方法,而方法不同,得到的结果也可能不同,解说参数的传递方式以前,让我们先以「变数的四个组成元素」来表示实际参数及形式参数的关系,就拿前面程式为例吧:
图-2 所谓传递参数,传的到底是名称、资料型别、位址、还是值呢? 就图-2来看,变数有四个元素,所谓传递参数,传的到底是名称、资料型别、位址、还是值呢? 其实除了传递「资料型别」无法达到传递参数的目的之外,其他叁个元素都可以作为参数传递的标的物,不过VB 并不支援传递「名称」的方式,以下笔者就针对传「值」及传「位址」两种方式来加以解说。 传值呼叫 这种传递参数的方式,是把呼叫者的实际参数值,复制一份到副程式的形式参数位址内,尔後副程式不管怎麽对形式参数进行运算,完全不会对实际参数的变数值有所影响。
图-3 传值呼叫 以前面的例子来说,呼叫程式端在实际参数的前後加上 (),例如「Call AddOne( (i) )」,其传递方式就是传值呼叫,所以以下的呼叫: i = 10 变数 i 的值不会因执行了 AddOne() 而被改变,因此输出结果等於10。 传位址呼叫 所谓传值呼叫,我们可以把它想成「以实际参数的值取代形式参数的值」,而传位址呼叫则是「以实际参数的位址取代形式参数的位址」,示意图如图-4:
图-4 传址呼叫 从图-4来看,我们可以把传位址呼叫中的实际参数与形式参数想成「同一个变数」(名称虽不同,但实际所占用的是同一块记忆体)。 以前面的例子来说,Call AddOne( i ) 由於未在变数 i 的前後再加上(),被视为传位址呼叫,因此呼叫之後的i值将因为形式参数x(与i为同一变数)的改变而跟着改变,所以以下的叙述: i = 10 i 的输出结果等於11。 ByVal 保留字与传值呼叫 在呼叫程式端,利用左右括弧将参数框起来,可使参数传递成为传值呼叫,此外,我们也可以在副程式(函数)定义端,利用ByVal 保留字将参数传递设定成传值呼叫,例如副程式定义如下: Sub AddOne( ByVal x ) 则不管「Call AddOne( (i) )」或是「Call AddOne( i )」均被视为传值呼叫。 传值呼叫的限制 在VB里面,「阵列」的传递是不可以采用传值呼叫的,例如以下的呼叫会产生错误: ' SubX 副程式的定义 传递阵列时,VB 之所以不接受传值呼叫的理由是,传值呼叫必须「复制」整个阵列,这对於大型阵列(例如含有30000个阵列元素),是一件比较不符合效率的事情,反之,若采用传址呼叫,以上面的程式为例,只要将 X 阵列的位址设定给 data 参数即可,而不必大量复制阵列的资料。 传址呼叫的限制 就像传值呼叫有其限制一样,传址呼叫也有其限制。传址呼叫的限制是「不同资料型别的实际参数不可作为传递的参数」,例如: ' SubX 副程式的定义 为什麽不接受呢?请参考图-5:
图-5 传址呼叫不接受不同型别的资料传递 假设「Call SubX( L )」是正确的,那麽将会造成:「Integer型别的变数I,其位址却指向一块型别为Long 的记忆体」,这显然违反了程式运作的原则,所以会产生错误。 不过,如果我们在副程式定义端,将形式参数宣告成Variant(不定型)或者不宣告资料型别,则该参数可以接受任何型别的资料,因为Variant的资料型别(及不定型型别)可随着资料来改变,如图-6:
图-6 传址呼叫不接受不同型别的资料传递 |