您的位置:寻梦网首页编程乐园VB 编程Visual Basic 电子教程>VB 程式设计内功讲座

VB 程式设计内功讲座(一)  /王国荣


近几年来,视觉化软体开发工具大行其道,尤其自"Visual" Basic 成功以来,其他软体开发工具亦纷纷起而效之,除了在名称上冠上Visual之外,视觉化设计亦无不成为必备之功能,宛如视觉化设计就是软体开发的万灵丹。

笔者相信视觉化设计可协助我们更快速地开发出应用软体,但不可否认的,软体的开发过程中,最常遭遇的问题还是:程式有没有bug、执行效能如何、是否符合使用者的需求…等,而如何克服这些问题则取决於程式设计者的功力,而不在视觉化开发工具上面。

怎样加强程式设计的功力呢?闭门苦修个叁年五载?那倒不必,有没有速成班呢?笔者觉得没有,但的确有若干重要的环节,若能打通,则可缩短苦练的功夫,本期就让笔者为您节说这些环节。


本文大纲


变数

    变数,这应该是基础的基础吧,这麽简单的东西也能够增强程式设计的功力?如果您这麽想,那就大错特错了,变数犹如程式中的血液,如果程式病了,最可能的原因就是血液遭受病毒的侵袭,不彻底研究血液,便很难把生病的程式治好。

组成变数的四大要素

    什麽是变数?我们写一个数学方程式,假设是:

      X = Y + 10

    这里面就有两个变数,一个是X,一个是Y。X及Y在这里是变数的「名称」;而当Y等於2时,X等於 12,此时2及12分别是变数X与Y的「值」。

    以上是我们从数学方程式中所看到的变数,它包含两个元素:名称与值。若从电脑的角度来看,X的值12必须储存在某一个地方,也就是记忆体的某一个「位址」之下,所以位址又是变数的另一个元素。

    为了表达一个变数,只有以上叁个元素往往还是不够的,举例来说:

      A = "RUN!VB "

    这时候储存变数A(等於"RUN!VB")所需的空间,显然比储存变数X(等於12)所需的空间来得大,而且连操作两种变数的方式也会有所差异,例如我们可以这样写X/2,但如果我们这样写A/2就没有意义了,为了区别特性不同的变数,程式语言中的变数必须再增加一个元素「资料型别」,以前面的例子来看,变数X是整数(Integer)型别,而变数A是字串(String)型别。

    综合以上的讨论,我们可以用以下四个元素:「名称」、「资料型别」、「位址」、及「值」来表示一个变数,如图-1,在图-1中,由於「值」是储存在某一个「位址」的记忆体里面的,所以把它画在位址的框框里面。

    图-1 变数的四个组成元素

    利用以上四个元素来表示变数,可以解释一些平常想当然尔的叙述,如图-2的「X=Y+10」:(假设Y的值等於123)


    图-2 叙述「X=Y+10」的运算过程

    这里顺便介绍两个术语:r-value 及 l-value。 r-value 是 right value 的缩写,表示等号右边的变数,如例子中的Y,实质上代表的是变数的「值」,l-value 是 left value 的缩写,表示等号左边的变数,如例子中的X,实质上代表的是变数的「位址」。

    以上我们用变数的组成元素来解释简单的例子,看起来没有特别的价值,但这是变数的根本道理,它同时可以用来解说复杂、以及容易让人搞错的例子,请务必了解这几个元素的意义。

    了解变数的组成元素之後,就不容易被VB的「定型变数」及「不定型变数」给搞晕了,兹说明如下:

    ◆ 定型变数

    此一类型的变数在宣告时会指明变数的「资料型别」,例如:

      Dim X As Integer ' X 是整数型别
      Dim S As String ' S 是字串型别
       

    图-3 定型变数

    此一类型变数最大的特色在於其资料型别是「固定的」,也就是说,当我们宣告变数时,指定给它某一型别後,它就永远是那个型别,而且将来就只能与型别「相同」或「相容」的变数做运算,举例来说,以下的例子会出现错误:

      Dim X as Integer
      X = "这是字串"   ' 执行时会出现「型态不符合」的讯息
       

    ◆ 不定型变数

    什麽是不定型变数?请看以下例子:

      Dim X   ' 宣告一个变数 X,但未指定资料型别
      X = 123 ' 此时 X 的资料是「整数」型别
      ...
      X = "VB" ' 後来又变成了「字串」型别
       

    图-4 不定型变数在不同时间被指定成不同的资料型别

    变数X在某一个时间被指定成「整数」的资料型别,但是後来它又被指定成另一种资料型别—「字串」,像X这样,资料型别可以变来变去的变数就是「不定型」变数。

    不定型变数的缺点是执行效率比较差,为什麽呢?由於它的资料型别是可变动的,因此每次执行时必须先判断变数当时的资料型别,无形中增加了执行上的负担。

    不过您也别把不定型变数想得太糟糕,上述的「额外负担」对大部份的程式来说,其影响都相当有限,一般而言需要注意的地方是避免在费时较久的回圈中使用不定型变数,例如:

      Dim X
      Dim I
       
      For I = 0 To 30000

        X = "AAAA"

      Next I

    如果我们将 Dim X 及 Dim I 分别修改成 Dim X As String 及 Dim I As Integer,则执行速度将有明显的改善。

    反观不定型变数有什麽优点呢?首先是它比较接近人类的思考模式。谁规定变数一旦被指定成某一种资料型别後,就不能改变了,所以变数的型别可以变来变去本来就比较容易使用,此外,它也比定型变数更容易应用到物件导向的程式中,举个例子:假设我们在程式中会使用到许多物件,而这些物件各具有不同的资料型别,为了管理这些物件,我们想把它们集中放在一起,其中最简便的方式就是采用阵列,所以程式可能是:

      Set ObjArr(1) = 物件_1
      Set ObjArr(2) = 物件_2
      ...
      Set ObjArr(N) = 物件_N

    由於 ObjArr 是一个阵列,假如我们把它宣告成「定型变数」,那麽该将它定义成「物件_1」的资料型别、还是「物件_2」的资料型别,还是…,显然是行不通的,如果把这个阵列宣告成「不定型变数」,则阵列中的每一个元素就可以依程式执行时的状况来改变其资料型别,自然而然就满足了这个需求。

不同资料型别的「值」转换

    当我们将一个单精准度型别(Single)的数值9.8指定给一个「不定型」变数X时,此一不定型变数X除了「值」元素会变成 9.8 之外,「资料型别」元素则会变成 Single,但如果我们将同样的9.8指定给一个整数(Integer)型别的变数Y,结果又如何呢?此时由於Y为「定型」变数,其「资料型别」元素(等於 Integer)是不可变的,因此,9.8 必须被转换成整数,然後才指定到Y的「值」元素中。


    图-5 同一数值,指定给不同型别变数後的差异

    由於不同型别的变数能够互相指定与运算,所以我们经常可以在VB程式中看到「值」转换的例子,如:

       Dim I As Integer
       I = "123"  ' 字串 "123" 先转换成 123,再指定给 I
       I = 9.8    ' 9.8 先四舍五入转换成 10,再指定给 I

       Dim S As String
       S = 123   ' 123 先转换成 "123" 再指定给 S

       Dim D As Date
       D = "1997/7/1" ' "1997/7/1" 先转换成 #1997/7/1# 再指定给 D

    当然,我们也经常遇到转换失败的例子:

       Dim I As Integer
       I = "123A"   ' "123A" 无法转换成整数,将产生错误
       I = 99999    ' 99999 超过整数的最大值 32767,也会产生错误

    在此笔者想说明几个因为「值」转换所产生的陷阱:

    ◆ 值转换之陷阱一

       Dim L As Long, I As Integer, J As Integer
       L = I * J

    以上的「L = I*J」,乍看之下并没有问题,因为I及J为整数,均占有2 bytes,而L为长整数,占有4 bytes,因此I*J 怎样也不会超过 L 的数值范围,但实际上,当 I*J>32767或 I*J<-32768 时,就会产生「溢位」,这是怎麽一回事呢?

    L=I*J 虽然只是一个简单的运算式,但对电脑而言,却至少包含以下的运算过程:

      (1) I*J, 然後将结果储存於「暂存记忆体」中。
      (2) 将「暂存记忆体」的值指定给L。

    由於I及J均为整数型别,因此在以上的运算过程中,系统也会配置与整数型别等大小的「暂存记忆体」来储存 I*J 的结果,而如果 I*J 超出整数型别的范围(-32768~32767),则暂存於「暂存记忆体」时就会产生「溢位」的错误。解决的方法十分简单,只要将I或J的数值强制转换成长整数,再作运算即可,例如:

      L = CLng(I) * J

    以上的CLng函数就是将数值强制转换成 Long(长整数)型别的函数,由於CLng(I)为一长整数,因此系统进行 CLng(I)*J 运算时,会配置与长整数型别等大小的「暂存记忆体」来储存 CLng(I)*J 运算的结果,如此一来,就不会产生「溢位」的错误了。除了CLng函数之外,常用的数值型别资料转换函数还有:

    CByte 强制转换成 Byte 型别
    CCur 强制转换成 Currency 型别
    CDbl 强制转换成 Double 型别
    CDec 强制转换成 Decimal 型别
    CInt 强制转换成 Integer 型别
    CSng 强制转换成 Single 型别

    ◆ 值转换之陷阱二

       Dim X1, X2, X3

       X1 = "12"
       X2 = "34"
       X3 = 34
       Print X1 + X2 ' 结果等於 "1234"
       Print X1 + X3 ' 结果等於 46

    以上的 X1、X2、及X3 都是「不定型」变数,而执行前叁个叙述之後,它们的「资料型别」元素分别等於:

       X1:字串
       X2:字串
       X3:整数

    因此接下来的 X1+X2 将被视为字串的串接,所以结果等於 "1234",但 X1+X3,由於X3等於34,为一整数,所以 X1 的值会由 "12" 转换成12再与34相加,所以得到 46 的结果。

    此一陷阱虽然很容易识破,但也很容易疏忽,如何防止这种错误,笔者倒有个简单的方法:「资料的『串接』一律使用 '&'号,而不使用 '+' 号,『数值』的运算须先确认被运算的资料是否为数值型别,若不是数值型别,则利用Val函数将资料转换成数值」,例如:

      X1 = "12"
      X2 = "34"
      X3 = 34
      Print X1 & X3 ' 资料串接,结果等於 "1234"
      Print Val(X1) + Val(X2) ' 数值相加,结果等於 46
       

复合式资料

    复合式资料指的是由多笔或多项基本资料所组成的资料,典型的代表有「阵列」及「结构型资料」。如果以变数的四个组成元素来解析阵列,则可以表示成 图-6:

    图-6 阵列与变数的四个组成元素

    阵列其实是很简单的资料结构,以上面的 X 阵列为例,我们可以将它视为 X(1)、X(2)… 、X(N)等变数的集合体。

    接着让我们来看结构型的资料,假设有一结构型变数 X 其结构如下:

       Type StructX
           I As Integer
           S As String
           D As Date
       End Type
       Dim X As StructX

    若表示成「变数的四个组成元素」,则如图-7:

    图-7 结构型变数与变数的四个元素

    在以上的结构型变数中,一共含有4个「名称」元素— X、X.I、X.S、及 X.D,值得注意的是 I、S、D 为 X 的资料成员,因此不能直接使用 I、S、D 的名称来存取 I、S、D。

物件

    笔者过去经常在 Run!PC 讨论物件导向技术,也许您已经听过资讯隐藏(information hiding)、资料萃取(data abstraction)、继承(inheritance)、多型(polymorphism)… 等技术名词,今天笔者想抛开这些名词,从变数的四个组成元素来看物件。

    首先请看以下叙述:

       Dim X As Integer
       Dim OX As Object

    以上的X变数与OX变数有何区别呢?如果我们利用 VB 内建的函数 IsObject 来检验以上两个变数,结果 IsObject(X) 传回 False,而 IsObject(OX) 则传回 True,这表示 OX 为一「物件」变数,而X只是普通变数。但笔者必须特别强调一点,物件变数也是由变数的四个元素所组成的,如图-8:

    图-8 即使是「物件」变数,也一样含有变数的四个元素

    物件变数最特别的地方在於「值」元素,对一般变数而言,「值」元素会纪录着该变数的值,但对物件变数而言,却记录着「具体物件」之位址,如图-9:

    图-9 物件变数的「值」元素是用来记录具体物件的位址

    而特别值得注意的是,当我们利用「 Dim OX As Object」宣告一个物件变数时,OX 并不包含图-9中的「具体物件」。

建立具体物件的几个方法

    在VB程式中有以下几种方法可以让物件变数含有「具体物件」:

    方法一: Dim OX1 As Object    
    Set OX1 = New ClassName
    方法二: Dim OX1 As New ClassName
    方法叁: Dim OX2 As Object    
    Set OX2 = OX1
    方法四: Dim OX3 As Object    
    Set OX3 = 建立具体物件的函数 



    兹说明如下:

    ◆ 建立具体物件方法一

    方法一的两个叙述可分解如图-10:

    图-10 建立具体物件〈方法一〉解析图

    在方法一之中,比较值得注意的是New保留字,此一保留字的作用是建立一个 ClassName 类型的具体物件,而 ClassName 必须是已存在之「物件类别」(class),举例来说,Collection 是VB所提供的物件类别,而以下叙述可以建立一个Collection类型的物件:

      Dim X As Object
      Set X = New Collection

    ◆ 建立具体物件方法二

    方法二其实是把方法一的两个叙述合而为一,也就是说:

       Dim OX2 As Object
       Set OX2 = New ClassName

    完全等於:

       Dim OX2 As New ClassName

    既然如此,我们只要使用方法二来建立物件不就好了,何必了解方法一的叙述呢?其实方法一与方法二是有区别的,方法二「Dim OX2 As New ClassName」之中的 OX2 一开始就被定义成 ClassName 类别,所以是一个「固定类别」的物件变数,如果我们设定其他类别的物件给这个物件变数,则会产生错误,此一道理与「定型变数」完全相同。至於方法一「Dim OX1 As Object」之中的OX1则属於「不固定类别」的物件变数,所以下面的叙述都是正确的:

      ' 建立 ClassName1 类型的物件给 OX1
      Set OX1 = New ClassName1
      ' 重新建立另一个 ClassName2 类型的物件给 OX1
      Set OX1 = New ClassName2

    ◆ 建立具体物件方法叁

      Dim OX2 As Object
      Set OX2 = OX1 ' 将 OX1 的具体物件指定给 OX2

    此一方法的意义是把某一个物件变数的具体物件复制给另一个物件变数,若以变数的四个元素来表示,则如图-11:

    图-11 「Set OX2 = OX1」的内部运作过程

    以上的「Set OX2 = OX1」称为「物件设定叙述」(或物件指定叙述),在语法上,它只比「变数指定叙述」(例如 X = Y) 多了一个 Set 保留字,但是在系统内部的运作上,却复杂得多。

    ◆ 建立具体物件方法四

    在以上建立具体物件的方法中,方法叁是将一个已存在具体物件设定给另一个物件变数,而方法一及方法二则是利用 New 保留字为物件变数建立具体物件,但有些物件类别却无法使用 New 保留字来建立物件,例如 VB 所提供的 Picture 物件类别,此类物件类别通常会搭配有「建立具体物件的函数」来代为建立具体物件,例如以下是建立 Picture 类别物件的方法:

       Dim pic As Picture
       Set pic = LoadPicture ( "图档名称" )

    以上的 LoadPicture 就是建立 Picture 类别物件的函数。此一建立具体物件的方法通常与方法一及方法二是互斥的,例如 Picture 类别物件的建立,若使用 New 叙述就会产生错误:

       Dim pic As New Picture ' 将产生错误
     

破坏具体物件的方法

    就所占用的记忆体而言,物件变数所占用的记忆体是固定的,以 VB 现况而言,只占用4 bytes(够省吧!),但物件变数所指到的「具体物件」则可能占用大量的记忆体,因此若程式随意建立具体物件,使用完毕时又不将它破坏以归还给系统,就可能造成系统记忆体耗尽的现象。

    至於破坏具体物件,将记忆体归还系统的叙述则十分简单,如图-12:

       Set 物件变数 = Nothing

    此一叙述的作用若以变数的四个元素来表达,则如下:

    图-12「Set OX1 = Nothing」的内部运作过程

    请参阅以下副程式,想一想,假如我们没有执行「Set 物件变数=Nothing」,将会有怎样的後果:

    Sub SubX()
      Dim X As Object

      ' 建立 ClassX 的具体物件给 X
      Set X As new ClassX
      ' 结束副程式之前,忘了执行 Set X = Nothing
    End Sub

    结果以上副程式每被呼叫一次,就会建立一个具体物件,但它并不会在副程式结束前,将具体物件归还系统,所以被呼叫的次数越多,吃掉的记忆体就越多,而使得系统可用之记忆体越来越少。

控制元件(control)

    控制元件是VB之中非常重要的物件,但笔者必须强调一点,控制元件的行径与前面介绍过的标准物件有极大的差异,首先从控制元件的建立与破坏谈起,当我们在表单(Form)布置一个控制元件之後,将来只要这个表单被载入,控制元件就会跟着被载入(建立),而当表单被载出时,控制元件也会跟着被载出(破坏),完全不劳程式设计者费心,由於很多人是从控制元件开始学习 VB 的,以致後来使用标准物件时,很容易忘了执行「Set 物件变数 = Nothing」破坏具体物件。

    使用控制元件时,常见的问题是:「如何动态地建立控制元件?」,例如表单上一开始没有控制元件或只有少数的控制元件,而程式希望随着执行时的状况动态地建立所需之控制元件。如果使用上一个段落所介绍的方法,可能有人会撰写以下叙述:

       Dim X As Object
       Set X = New TextBox ' 建立 TextBox 控制元件

    但实际上是行不通的,为什麽?因为控制元件是表单的子物件,而利用「Set 物件变数=New 物件类别名」所建立的物件则属於「程式」所管辖,而VB并不允许我们建立附属於「程式」的控制元件,因此以上叙述会产生错误,但怎样在表单上面动态地建立控制元件呢?

    以TextBox控制元件的建立为例,至少必须在表单上先布置一个TextBox控制元件,并且设定它的Index属性,然後使用Load叙述建立控制元件:(假设此一 TextBox 名称为Text1,而Index属性设定为0)

       Load Text1(I) ' I 可以是 0 以外的正整数
       Text1(I).Visible = True ' 将 Text1(I) 显示出来
       ' 此外,可能还需要设定 Left、Top 属性以决定座标位置

    假设我们执行了以下叙述:

      Load Text1(1)
      Load Text1(3)

    则Text1阵列的「值」元素可以表达成图-13:

    图-13 经过 Load 叙述之後的「值」元素结构

    至於破坏Text1阵列中的某一个控制元件,方法则是:

       Unload Text1(I)

    以上被破坏之控制元件必须是利用程式建立出来的Text1(I),假设Text1(0)是设计阶段就已经布置在表单上面的控制元件,则「Unload Text1(0)」将会产生错误。

「Set X = 控制元件名」的意义

    在「建立具体物件的几个方法」的段落中,我们介绍过「Set 物件变数1=物件变数2」的叙述,此一叙述的作用是复制物件变数2的具体物件给物件变数1,在控制元件之中,也有类似的叙述「Set物件变数名=控制元件名」,例如:

       Dim X As Object
       Set X = Text1

    但它的作用不是「复 制」Text1的具体物件给X,而是将Text1的具体物件与X的「值」元素产生连结关系,如图-14:

    图-14「Set X = Text1」的意义

    也就是说,「Set X = Text1」并不会建立具体物件,而执行此一叙述之後,X与Text1指的将是同一个具体物件,所以以下叙述会将Text1的内容变成 "ChangeIt!":

       X.Text = "ChangeIt!"
       ' 作用相当於 Text1.Text = "ChangeIt!"
     

表单(Form)

    表单也是物件,但它又跟标准物件及控制元件又有些出入,以下让我们来比较几个操作表单的叙述:

    ◆ Load FormX:载入FormX。与「Load控制元件(I)」不同的是,「Load FormX」只能载入既有的表单,而无法动态地建立表单。此外请注意,载入并不等於显示出来,若要显示出来还要执行以下叙述:

       FormX.Visible = True  或
       FormX.Show

    ◆ FormX.Show:显示表单,若表单尚未载入,则此一叙述会先载入表单再显示。

    ◆ Unload FormX:载出表单。

    ◆ Set X = New FormX:复制FormX的具体物件给物件变数X,执行此一叙述後,X与FormX为两个独立的物件。(注:想要将X所指到的表单显示出来,还是要将 X.Visible设定为True)

    ◆ Set X = FormX:将X指到FormX的具体物件,也就是说,X与FormX指的是同一个表单物件,举例来说,若执行此一叙述後,接着又执行「X.Caption = "NewTitle"」,则FormX的标题将会变成"NewTitle"。

    ◆ Set X = Nothing:将X所指到的表单具体物件归还给系统。 

结语

    本期介绍了变数及物件在系统内部的组成,虽然没有程式设计的技巧,却是改善程式体质不可或缺的观念。了解变数及物件的组成,就好像了解血液是由红血球、白血球、血小板…所组成的一样,但血液不是静止的,同样的,变数及物件在程式中也不是静止的,静止的变数及物件是没有bug的,唯有到处流窜的变数及物件才会造成程式的错误,下一期笔者将介绍变数与物件在程式中的流窜模式。