4. 變數和程序

Microsoft Visual Basic 不只為你在撰寫你的應用程式時提供一個視覺化的使用者界面環境,它同時也包括了一個功能強大的程式語言-Visual Basic for Application(VBA),它讓您可以很容易的操控由其他應用程式建立的控制項、檔案、資料庫及物件等等。這個章節和下一個章節也會包含一些文件沒提及的特性和一些改善效能的方法。我們假設你已經熟悉了基本的程式撰寫,因此我們將不打算花太多的時間說明什麼是變數、整數和浮點數之間的差異,我們將著重在更多有趣的主題和 Visual Basic 6 新增的功能上。

變數的範圍和生命週期
 

並非所有的變數都是同時產生的,某些變數是隨著應用程式的結束而結束,而某些變數有可能在一個程式裡被宣告和釋放過無數次。一個變數可以只存在於一個程序或一個模組或只存在於應用程式裡的某個視窗中。為了讓這些概念更清楚,筆者想介紹以下二個基本定義。

  • 一個變數的作用範圍是程式碼中該變數能被存取的部份。譬如,在程式中,一個被宣告成 Public 的變數可以在任何地方被存取。反之,如果這變數是被宣告成 Private 的,則該變數只能在宣告它的那個模組裡才看得到。
     
  • 一個變數的生命週期取決於該變數停留在記憶體中的時間。之前提到過的 Public 變數的生命週期一般來說和應用程式的生命週期是一致的,但也未必都是如此。比如,一個程序中的一個局部動態變數是必須當 Visual Basic 執行該程序時才會被建立的,當該程序結束時則該變數也會跟著被釋放掉。
     

Global (全域) 變數
 

在 Visual Basic 的術語裡,Global 變數是那些於一般模組(附檔名為 .bas)裡,以 Public 這個關鍵字所宣告的變數。基本上,這些變數是最簡單的一群,因為它們是隨著應用程式被執行而產生的,其作用範圍也達到整個應用程式。(換句話說,它們能夠在程式的任何地方被讀取或修改)。以下的程式片斷說明如何宣告一個 Global 變數:

'在一般模組裡
Public InvoiceCount as Long    ' 宣告一個 Global 變數

Visual Basic 6 仍然支援 Global 這個關鍵字,用以跟 Visual Basic 3 或再之前的版本相容,但是 微軟並不建議在現在的VB中再去使用。

一般而言,使用太多的全域變數並不是寫程式的一個好習慣。所以如果可能的話,應該盡量使用模組層次或區域變數,因為他們可以一直的重覆使用。但是如果你的模組和個別的常式是依賴全域變數在做溝通的話,那你就應該避免去重覆定義這些已經定義過的全域變數。然而實際上,在寫應用程式的時候很少會不去使用到全域變數的,所以我的建議是:謹慎的使用全域變數,並且在取變數名稱時最好能夠很清楚的就分辨出來(譬如用一個 g_ 或 glo 當開頭)更重要的是,在每個程序裡清楚的註明正在用的全域變數有那些。

另一個筆者覺得也滿有用的方法,就是在一般模組裡定義一個 GlobalUDT 的使用者自定的資料形別, 並將應用程式裡所有用到的全域變數定義在裡面,, 之後再另外宣告一個資料形別為 GlobalUDT 的全域變數。

' NOTE: this procedure depends on the following global variables:
'      g_InvoiceCount  :  number of invoices (read and modified)
'      g_UserName    :  number of current user (read only)
Sub  CreateNewInvoice( )
     ...
End  Sub
' 在一般模組裡
Public Type GlobalUDT
    InvoiceCount As Long
    UserName As String
    ....
End Type
Public glo As GlobalUDT

你可以用一個非常簡潔清楚的語法去存取這些變數:

' 可以用在應用程式的任何地方
glo.InvoiceCount = glo.InvoiceCount + 1

這技術有許多優勢。第一,經由它的名稱可以很清楚的知道這變數是用在什麼地方的,假如你忘記了變數的名稱,那你可以只打 glo ,然後VB的智慧感知(Intellisense)功能 就會幫你列出 glo 之下所有的成員變數。在大多數的時候,你可以只打前面幾個字母,然後 Visual Basic 就會幫你拼出完整的名稱出來。這將會幫你節省很多的時間。

另一方面,你也可以很容易的將你的變數存到一個資料檔裡。

' 將經常性的 Global 變數存取動作寫成
Sub SaveLoadGlobalData(filename As String, Save As Boolean)
    Dim filenum As Integer, isOpen As Boolean
    On Error Goto Error_Handler
    filenum = FreeFile
    Open filename For Binary As filenum
    isOpen = True
    If Save Then
        Put #filenum, , glo
    Else
        Get #filenum, , glo
    End If
Error_Handler:
    If isOpen Then Close #filenum
End Sub

這方法漂亮的地方在於你能夠很容易的去新增和移除這些變數(實際上是 GlobalUDT 這個資料結構的成員物件)而不去更改到 SaveLoadGlobalData 這個常式(當然,你必須使用相同的版本去儲存 GlobalUDT,才能正確的載入。)

模組層變數
 

假如你在一個模組(一般模組、表單模組、物件類別模組等等)的宣告區宣告一個 Dim 或 Private 變數,那你就算建立了一個私用的模組層變數。這類的變數只存在於自己所屬的模組之內,並不能從其他地方存取。通常,這些變數只有在相同模組的程序中才有作用。

' 在任何模組的宣告區
Private LoginTime As Date     ' 宣告一個模組層的 Private 變數
Dim LoginPassword As String   ' 其他的模組層 Private 變數

除了一般模組之外,你也能將模組層變數宣告為 Public(因為在一般模組所宣告的 Public 變數視為全域變數)。模組層的 Public 變數能夠被相同模組裡的任何一個程序存取,它也能被其他的模組從外面存取。

' 在 Form1 的宣告區宣告
Public CustomerName As String          ' 宣告一個 Public 屬性

你可以像是從外面存取一個模組的屬性般的存取一個模組的變數

' 在 Form1 外面存取...
Form1.CustomerName = "John Smith"

一個模組層變數的生命週期是與它所屬模組的生命週期一致的。Private 變數在一般模組的生命週期也是整個應用程式的生命週期,即使它們只能在當 Visual Basic 執行到該模組的時候才能被存取。而表單和物件模組的變數只有當該表單或模組被載入到記憶體後才存在。換句話說,當一個表單正被執行(但使用者不一定看得見),則該表單裡的變數便佔用了一部份的記憶體空間,當該表單結束後,這些存在表單裡的變數才會從記憶體裡被釋放。當下一次該表單再被重新載入時,Visual Basic 才會為表單裡的變數重新分配記憶體空間並且重設它們的初值(數值型態設為 0,字串型態設為 "",沒有指定的則預設為物件型態變數)。

動態區域變數
 

動態區域變數被定義在一個程序裡;它們的範圍和生命週期都僅只限於該程序:

Sub PrintInvoice()
    Dim text As String      ' 這是一個動態區域變數
    ...
End Sub

某程序只要被執行一次,則該程序裡的動態區域變數就會被重新產生並重新指定一次它的預設值(數值、空字串,或 Null)。當該程序結束離開後,則 Visual Basic 將對記憶體重新配置並將變數釋放。

靜態區域變數
 

靜態區域變數是一個混合型,因為他們有跟區域變數一樣的範圍,和跟模組層變數一樣的生命週期。在呼叫到它們所屬的程序後,它們的值會一直存在,直到它們所屬的模組被釋放為止(在一般模組的程序裡,它們會一直存在直到應用程式結束)。

在一個程序中,靜態區域變數是以 Static 這個關鍵字宣告的。

Sub PrintInvoice()
    Static InProgress As Boolean   ' 靜態區域變數
    ...
End Sub

你可以宣告整個程序是 Static 型態,則程序之內宣告的所有變數也將是 Static 型態的。

Static Sub PrintInvoice()
    Dim InProgress As Boolean      ' 這也是靜態區域變數
    ...
End Sub

靜態區域變數有點類似模組層的 Private 變數,你可以將一個程序有關靜態宣告的部份移到模組的宣告區裡(你只須要將 Static 改為 Dim,因為 Static 不允許宣告在程序之外),而不會影響程序的正常動作。假如一個變數只會在某一個程序裡用到,那我們會建議將該變數宣告為一個靜態的程序層變數。就某方面而言,在不需要和其他的程序共用變數的前提下,其宣告成靜態區域變數或宣告成模組層變數是一樣的。在程序裡多運用這樣的變數宣告,你便可以很容易的做到程式碼的重覆使用。靜態變數的宣告能夠幫助我們避免一些不可預期的動作,尤其是在處理一些事件程序上常是有必要的,譬如你想避免使用者重覆按下某個按鈕。

如下面的程式碼:

Private Sub cmdSearch_Click()
    Static InProgress As Boolean
    ' 程序進入後, 作用指標 InProgress 設為 True 以避免重覆執行
    If InProgress Then MsgBox "Sorry, try again later": Exit Sub
    InProgress = True
    ' Do your search here.
      ...
    ' 程序結束後重設作用指標為 False
    InProgress = False
End Sub

資料型態總論
 

Visual Basic 支援的資料型態包含了整數、浮點數、字串、日期時間等等..

你能將資料儲存在適當類型的變數裡,或使用 Variant 資料型態 - VBA 裡的預設資料型態,一種可以是任何類型資料的資料型態。

整數資料型態
 

整數變數的範圍從 -32768 到 32767,每個整數占記憶體 16-bit(位元)也就是 2 bytes(位元組)的空間,直到 32 位元視窗平台的 Visual Basic 版本的出現,整數變數可說是最常用到的變數型態。事實上,在一個 32 位元的環境,你可以使用一個長整數變數代替一個整數變數,當一個變數值大得超過了有效範圍時,可以減少溢位錯誤發生的機率。在某些需要建立一個很大的陣列的時候,應該優先考慮以長整數來宣告,至於其他的時候,除非你有很好的理由(例如呼叫一個外部程式或一個 dll 而必須使用到整數),否則我們也建議你使用長整數。


說明

你也可以間接地指定一個未經宣告類型的變數是整數型態,只要在其名稱前面加一個 % 記號以及其他型態的變數也適用這樣的宣告方式,如長整數變數 Long(&)、單精變數 Single(!)、雙精變數 Double(#)、貨幣型態變數(@)、字串變數($),但這個功能在VB6中最主要只是提供與前版VB和QuickBasic之間的相容性,所有新建立的應用程式都應該盡量使用明確的方式宣告資料型態。


你程式中所有的整數常數都預設為整數型態(interger),除非變數的值超出了整數的範圍,則將被存成長整數型態。

長整數資料型態
 

長整數的範圍從 -2147483648 到 2147483647 ,每個長整數占記憶體 32-bit(位元)也就是 4 bytes(位元組)的空間。就像前面提到的,我們盡量建議讀者將整數的資料型態多設成長整數型態,因為長整數變數處理起來要比整數快,且在處理大數字的時候也可以避免溢位的發生。例如,當你必須處理一個大於 32767 個字元長度的字串時,你就必須用一個長整數變數來代替整數變數,如果你是用較舊的 Visual Basic 版本寫的,在轉換時就要特別的注意。就像先前提到地,我們建議你盡量不要以加 &字元的方式宣告長整數,但是無論如何,它是一個可以強迫編譯器將整數變數當成長整數變數處理的方法,這些差別有時候有可能是很重要的。

Result = value And &HFFFF     ' 這裡的 &HFFFF 表示 -1 
Result = value And &HFFFF&    ' 這裡的 &HFFFF& 表示 65535

有時候你只是想定義一個常數變數則可以寫成:

Const LOWWORD_MASK As Long = &HFFFF&

注意

為了先前版本的需求,Visual Basic 讓你可以以 Deftype 這個指令來指定變數的預設資料型態,因此你可能會在每個模組一開始的地方使用 DefLng A-Z 來宣告長整數變數,以確保每個長整數變數都有被宣告到。我的忠告是:千萬別這麼做!

使用 Deftype 指令來宣告你的變數是個滿危險的動作,Deftype 指令並且會損害到程式碼的可重覆使用性,你不能確定將程式碼自一模組 Copy 或剪貼到另外一個沒有相同宣告的模組還能正常的動作。


布林資料型態
 

布林變數只是個 0 和 -1 的值,分別代表假和真。當你使用一個布林變數,事實上你有 15 ~ 16 位元是浪費掉的,因為它所代表的訊息以一個位元就可以很容易地表達了。雖然這麼說,但還是不建議你用整數變數代替布林變數,因為使用布林變數可以增加程式碼的可讀性,在某些時候,對於系統的效率也會有點幫助,但是這並不是最主要的因素。

位元組資料型態
 

位元組變數的範圍是介於 0 ∼ 255 之間的整數,每個位元組變數佔了 8位元的長度,也是 Visual Basic 允許資料的最小長度。Visual Basic 4 引入位元組資料型態,使得 16 位元的應用程式能夠很容易的移植到 95 和 NT 的平台。他們特別將字符集存成 Unicode 代替 ANSI 的格式,以使 32位元平台的 Visual Basic 4 的應用程式可以相容於 16位元平台的 Visual Basic 3 和 Visual Basic 4 的應用程式。這一點差別又產生了另一個傳字串值給 API(視窗程式界面)函數的問題,因為 Visual Basic 3 的程式設計師是使用二進制的字串資料,但 Unicode-to-ANSI 能夠經由 Visual Basic 而自動的轉換執行。

簡單的說,將位元組資料型態加到 Visual Basic 裡主要就是要解決這樣的問題。除此之外,你會用到位元組資料型態只有在你處理二進制資料型態陣列上才用得到了。如果只是一個單一的數值,使用整數或長整數變數一般來說是較好的選擇。

單精資料型態
 

單精變數的範圍,負數是介於 -3.402823E38到 -1.401298E-45之間,正數則是介於1.401298E-45到3.402823E38之間,每個單精變數佔用4位元組的記憶體空間,這也是Visual Basic允許的浮點資料型態的最少長度。

很多程式設計師相信,單精變數不會比雙精變數快,至少在大多數的視窗環境裡,其原因在於大部份的系統,所有的浮點運算都是數學輔助運算器另外獨立出來運算的,意思就是說在大部份的時候你應該用雙精變數,因為它們提供一個更好的精度、更廣的範圍、更少的溢位問題而且不會影響系統表現。

當你在處理大的浮點陣列,或是當你在表單上需要用到PictureBox來處理圖形的時候單精資料型態會是個不錯的選擇。事實上,在座標上(包括點、線、圓、ScaleWidth、ScaleHeight等等)都是使用單精資料型態來表示的。

雙精資料型態
 

雙精變數的範圍,負數是介於 -1.79769313486232E308到 -4.94065645841247E-324之間,正數則是介於4.9406564581247E-324到1.79769313486232E308之間,每個雙精變數佔用8 bytes的記憶體空間,當在處理十進制值的時候用雙精資料型態會是比較好的選擇。Visual Basic有一些內建函數的回傳值是雙精資料型態的,譬如Val這個函數,即使是不包含小數點的數也是如此。

字串資料型態
 

所有32位元平台的Visual Basic(32位元的VB4和VB5、6)都是以Unicode儲存字串字元,而先前的版本則是使用ANSI的格式,會造就Unicode這樣的寫碼方式,最初主要是為了顯示非拉丁語字母(諸如:中文、日文、希伯來文)的訊息上,如果你不將你的軟體本土化,你將看著Unicode的字串在你的程式中虛耗你的記憶體,特別是如果你使用了很多的長字串。

Visual Basic在字串處理上分為二個類型,一是傳統的可變長度字串,一是固定長度字串,分別用不同的宣告方式宣告它們:

Dim VarLenStr As String
Dim FixedLenStr As String * 40

第一個明顯的差別在於,任何一個給定長度的字串變數只會佔記憶體其處理該字串基本所需的空間(事實上,它另外多取了10個bytes用來儲存有關該字串的其他資訊,包括它的長度),如果你是個精明的程式設計師,你或許還記得使用可變長度字串通常要比使用固定長度字串快些,那是因為VBA的字串函數只能對可變長度字串做處理,當你傳給VBA函數一個固定長度字串,編譯器會將它放在一個暫時的可變長度字串變數裡。

雖然這麼說,但使用固定長度字串未必會使你的程式變慢。Visual Basic也擅長於對固定長度字串的記憶體分配,如果你的程式常需要花時間在變數的指派或大字串陣列的處理,則使用固定長度字串來作甚至可能比傳統的方法更快。舉個例子,如果是一台233KHZ的電腦, Visual Basic 6載入一個有100000個元素, 每個元素30長度的字串陣列大約要花9秒,而從記憶體中移除則須0.4秒。如果使用固定長度的字串陣列來作,則兩個動作幾乎是同時完成的。

你可以用雙引號將一字串括起來代表字串常數, 表示法如下:

Print "<My Name Is ""Tarzan"">"     ' 結果會顯示 <My Name Is "Tarzan">

Visual Basic另外定義了許多內建的字串常數,例如: vbTab(跳位字元)或vbCrLf(換行字元),使用這些常數能夠增加你程式碼的可讀性和系統效能;因為如此你就不需再使用Chr函數去產生一個字串了。

貨幣資料型態
 

貨幣變數的範圍從 -922,337,203,685,477.5808到922,337,203,685,477.5807,不同於浮點變數的是,貨幣型態資料只顯示到小數第4位。使用固定小數值有它方便的地方,其一就是比較沒有四捨五入的問題。然而,如果是做數學的運算,使用貨幣型態變數就比使用雙精型態變數要慢了。

日期資料型態
 

日期變數能夠顯示從100/1/1到9999/12/31的任何日期值,每個日期變數都占了8 bytes的記憶體空間。日期時間變數在內部是以浮點數的方式儲存的,整數部分儲存的是日期資訊,小數部份則儲存時間資訊(譬如,12AM表示成0.5,6PM則表示成0.75等等)一旦你了解了日期變數在電腦內部的儲存方式,你就能運用數學的方式操作它們了。譬如,你能使用Int這個函數去截斷日期或時間資料,如下所示:

MyVar = Now                  ' MyVar是一個日期變數
DateVar = Int(MyVar)           ' 截取日期資訊
TimeVar = MyVar - Int(MyVar)   ' 截取時間資訊

你也能將日期當成數字一樣的做加減運算

MyVar = MyVar + 7              ' 提前一個禮拜
MyVar = MyVar - 365            ' 回溯一年

VBA提供了很多處理日期時間的函數,在 第五章 中會有比較詳細的介紹,你也可以將一個日期常數用 #mm/dd/yyyy# 的格式定義,時間部分可有可無:

MyVar = #9/5/1996 12.20 am#

物件資料型態
 

Visual Basic使用物件變數儲存物件參考。應該注意的是,我們講的是儲存一個物件參考,而不是直接儲存一個物件,這個差別是很重要的,我們將在 第六章 中討論。物件變數有幾個類型,一般的物件變數和特殊的物件變數,這裡是一些例子:

' 一般物件變數範例
Dim frm As Form             ' 一個表單物件參考
Dim midfrm As MDIForm     ' 一個多重文件介面表單物件參考
Dim ctrl As Control          ' 一個控制項物件參考
Dim obj As Object           ' 一個物件物件參考

' 特殊物件變數範例
Dim inv As frmInvoice         ' 一個表單的特殊物件參考
Dim txtSalary As TextBox      ' 一個控制項的特殊物件參考
Dim cust As CCustomer       ' 某類別模組定義的物件參考
Dim wrk As Excel.Worksheet  ' 一個外來的物件參考

處理物件變數和一般變數比較明顯的差別在於物件變數必須用Set這個關鍵字去指定你的物件參考,如下面的程式碼:

Set frm = Form1
Set txtSalary = Text1

指定之後你就能用你所指定的物件變數來存取原來物件的屬性和方法:

frm.Caption = "Welcome to Visual Basic 6"
txtSalary.Text = Format(99000, "currency")

注意

大部份程式設計師在處理物件變數上都會犯的毛病就是省略了SET命令,假如你省略了這個關鍵字,則Visual Basic會產生一個編譯時期的錯誤(Invalid use of property─無效的屬性)。

frm = Form1           ' A missing Set raises a compiler error.
txtSalary = Text1        ' A missing Set assigns Text1's Text property
                      ' to txtSalary's Text property.

物件變數也是可以被清除的,清除之後他們便不再指到任何特定的物件上了。

Set txtSalary = Nothing

Variant資料型態
 

Variant變數是在Visual Basic 3開始被介紹,但到了Visual Basic 4的時候功能有了大輻度的增強。Variant變數的格式是以OLE方式定義的,所以在未來,它的格式極有可能再改變,但到目前為止,Variant變數能夠存任何型態的資料。一個Variant變數佔了16 Bytes的記憶體空間,如圖示:


 

第0和第1個Bytes存放著用來表示第8到第15 Bytes所存資料類型的整數,第2到第7個Bytes並沒有用到,而且大部份的時候,另一半也不是全部都會用到。Variant變數將一個Visual Basic支援的任何格式的值以它原來的格式存入。譬如,當Visual Basic把兩個Variant變數值相加,它會先檢查變數的類型,並盡可能的使用最有效率的數學常式去計算。所以,假如你把一個Variant整數變數和一個Variant長整數變數相加,Visual Basic會將這整數變數轉成長整數變數,並當成兩個長整數去處理它。


注意

自動的資料轉換總是比較危險的,因為有可能得到的結果並不是你所預期的。譬如,你使用 + 這個符號在兩個數值之間,Visual Basic會把這 + 當成加法運算子,如果兩邊都是字串,Visual Basic會把兩個字串相加變成一個字串。但是如果一邊是字串而另一邊是數字的話,Visual Basic就會先嘗試把字串轉換成數字相加,如果不成功,就會產生一個「 資料型態不符」的錯誤。所以如果你想要確定一個相加的動作被執行而不需要考慮資料型態,你應該使用 & 這個運算子。最後要注意的是,你不能將一個固定長度的字串存進Variant變數裡。


Variant資料型態是Visual Basic預設的資料型態。換句話說,如果你使用一個變數而沒有宣告它的型態,像下面這一行程式碼:

Dim MyVariable

則該變數就是一個Variant變數,除非你有特別指定設成某一個資料型態,如此會成為自由型態(Variant)變數,除非在此行之前有一個設定不同預設資料類型的Deftype指示。同樣的,如果你使用一個變數而不事先宣告(而且你並沒有使用Deftype指示),Visual Basic會建立一個自由型態變數。


說明

我們給初級Visual Basic程式設計師們的建議是:

在你程式每一個模組一開始的地方都加上Option Explicit的指令,最好是在工具列的「工具/選項/編輯器/要求變數宣告」選項打勾,這樣Visual Basic就會在你每次建立一個新的模組時自動加上這行指令。


一個Variant變數的真正資料型態取決於最後一次指派給該變數的資料型態,你可以用VarType函數測試一個Variant變數目前真正的資料型態:

Dim v As Variant
v = True
Print VarType(v)     ' 結果會印出 "11" , 代表Boolean資料型態

Variant變數也可以指定不屬於任何資料型態的值。如果沒有指定任何值給一個Variant變數,則它的預設值是空值(Empty),你可以用IsEmpty這個函數測試某個變數是否為空,或使用VarType這個函數,則傳回值為0。

Dim v As Variant
Print IsEmpty(v)       ' 結果會印出 "True" 因為未指定任何值
v = "any value"       ' 該變數已經不再是空值
v = Empty            ' 重新將該變數設成空的狀態

Null對資料庫的程式撰寫很有幫助,由其是在你未指定任何欄位值的時候。你可以用Null這個關鍵字將一個變數設成Null的狀況,如果要測試某變數是否為Null,可以用IsNull這個函數,或使用VarType這個函數,則傳回值為1。

v = Null             ' Stores a Null value
Print IsNull(v)       ' Prints "True"

Variant變數也可以是一個Error值。這是很有用的,用來測試傳回值是否為error值,用以判斷你所要求的動作是否成功。你可以用CVErr這個函數建一個自定的函數來提示錯誤。

Function Reciprocal(n As Double) As Variant
If n <> 0 Then
    Reciprocal = 1 / n
    Else
        Reciprocal = CVErr(11)    ' 括號內是你想要顯示的錯誤碼
    End If
End Function

如果要測試某變數是否為error,可以用IsError這個函數,或使用VarType這個函數,則傳回值為10,錯誤碼的範圍從0到65535,如果要將錯誤碼轉換成一個整數,你可以使用CLng這個函數,下面是一個傳回錯誤碼的例子:

Dim res As Variant
res = Reciprocal(CDbl(Text1.Text))
If IsError(res) Then
    MsgBox "Error #" & CLng(res)
Else
    MsgBox "Result is " & res
End If

不過你寧可多利用內建的錯誤處理物件來處理錯誤,因為它能傳達更多關於錯誤的資訊。Variant變數也可以連結物件,但是你必須用Set這個關鍵字來指定一個物件Variant變數,否則下面的例子就會出錯:

Dim v As Variant
Set v = Text1         ' 用Set指定一個物件Variant變數
v.Text = "abcde"
v = Text1           ' 錯誤的物件宣告, 因為少了Set 
                  ' 其結果會等於v = Text1.Text
Print v            ' 印出 "abcde"
v.Text = "12345"   ' 產生錯誤碼Error 424: v已經不是物件了

你可以用IsObject函數來測試一個Variant變數是否是一個物件,但這裡VarType這個函數就不適用了,因為如果該物件支援預設的屬性,則VarType函數只會傳回該屬性而不是一個物件。

Visual Basic 6開始Variant變數也可以指定使用者自定的資料型態了,而用VarType測試將傳回36這個新值。Visual Basic 6開啟時,自由型態變數也可以保留user-defined type (UDT)結構,VarType函數會傳回36-vbUserDefinedType新值。但此功能只有在定義UDT指令的Type敘述與Public範圍一起顯示在Public類別模組時,才能夠使用。你無法將UDT結構指派給Standard EXE專案內的自由型能變數,因為指令無法在Public類別模組內顯示。

你也可以使用其他的函數去測試一個Variant變數值的型態。例如使用CDbl函數轉換一個變數值成功,則用IsNumeric測試該變數會回傳True值,或用CDate函數轉換一個變數成日期型態,則可以用IsDate測試該變數值。最後一提的是TypeName函數有點類似VarType函數,差別在於它所傳回的是一個可判讀的字串。

v = 123.45: Print TypeName(v)       ' 會印出 "Double"
Set v = Text1: Print TypeName(v)    ' 會印出 "TextBox"

Variant變數也可以存成陣列資料型態,我們將放在後面有關陣列的部份討論。

Decimal資料型態
 

Decimal是一個精度比Double資料型態還高的浮點資料型態,但是其範圍較小。

範圍在正負79228162514264337593543950335 (沒有小數點),或正負7.9228162514264337593543950335小數點後28位。Decimal型態中,最小的非0數是正負0.0000000000000000000000000001,在Visual Basic中你不能明確的宣告一個變數為Decimal型態,但是當你指定一個值到一個Variant變數後,可以使用Cdec函數將內容轉成Decimal型態,譬如:

Dim v As Variant
     v = CDec(Text1.Text)

之後該變數就可以進行一般的數學運算,你可以不用考慮運算元兩邊的值是不是都是Decimal型態,因為Visual Basic將會幫你做一些必要的轉換。如果你用VarType函數去測試一個Decimal型態的Variant變數,則會得到一個14的傳回值。

整合資料型態
 

到目前為止我們檢閱的內含資料型態都相當簡單。然而有效地使用它們也可以建立一個複合資料型態。

使用者自訂資料型態
 

(UDT)是一種複合的資料結構,在開始使用UDT變數之前,必須先定義它的結構。

在模組的宣告區使用Type指令定義。

Private Type EmployeeUDT
     Name As String
     DepartmentID As Long
     Salary As Currency
End Type

UDTs可以被定義成Private或Public的。在VB5或之前的版本,只有定義在一般模組下的UDTs可以宣告為公用的。而VB6裡,除了表單外的所有模組都可以包含公用UDT定義,提供非標準 .EXE的專案及非Private的類別。

一旦你定義了一個Type的結構,你就可以像建立任何一個VB一般型態變數一樣地建立這個資料型態的變數。然後你就能用其語法存取它的個別項目。

Dim Emp As EmployeeUDT
Emp.Name = "Roscoe Powell"
Emp.DepartmentID = 123

UDT可以包含傳統和固定長度的字串。前一個結構在記憶體中只存放實際資料的指標,而後一個字串的文字是與UDT結構裡的其他項目存放在同一個區段中。從LenB函數可以看出來。你可以使用任何一個UDT變數去得知它們使用中實際的位元組。

Print LenB(Emp)     ' 列出16: 4給名稱=而不管其長度 +
                   ' 4給DepartmentID (Long) + 8給Salary (Currency)

Type結構也可以包含子結構,如:

Private Type LocationUDT
     Address As String
     City As String
     Zip As String
     State As String * 2
End Type
Private Type EmployeeUDT
     Name As String
     DepartmentID As Long
     Salary As Currency
     Location As LocationUDT
End Type

當你存取巢狀結構時,可以採取With...End With方式撰寫程式碼。

With Emp
     Print .Name
     Print .Salary 
     With .Location
         Print .Address
         Print .City & "  " & .Zip & "  " & .State
     End With
End Type

當你使用一個複合的UDT,指定一個個別元件的值常常是件很討厭的事。幸運地,因為VBA支援回傳UDT的函數。所以你可以寫一些程序來大量簡化你的工作。

Emp = InitEmployee("Roscoe Powell", 123, 80000)
 ...
Function InitEmployee(Name As String, DepartmentID As Long, _
     Salary As Currency) As EmployeeUDT
     InitEmployee.Name = Name 
     InitEmployee.DepartmentID = DepartmentID
     InitEmployee.Salary = Salary
End Function

VB允許你用一般的指定方法來複製一UDT結構到另一個UDT,如以下程式碼:

Dim emp1 As EmployeeUDT, emp2 As EmployeeUDT
 ...
emp2 = emp1

陣列
 

陣列是一組有次序的同質項目。VB支援由基本資料型態組成的陣列。你可以建立一維陣列、二維陣列等等,多達六十維度(雖然不曾見過哪一個程式設計師實際應用過這麼多)

靜態和動態陣列
 

基本上,你可以建立靜態或是動態的陣列。靜態陣列必須包含固定數目的項目。而且這個數目必須在編譯程式時就定義好,如此,編譯器才能配備好所需的記憶體空間。用Dim來宣告一個靜態陣列。

' 這是一個靜態陣列
Dim Names(100) As String

VB從零開始編譯陣列的索引。因此,上面這個陣列包含了101個項目。

大部份的程式不使用靜態陣列,因為寫程式的人很少在程式編譯時就知道需要多少項目,而且靜態陣列在執行時是不能任意改變大小的。動態陣列解決了以上這兩個問題。用兩個步驟來宣告和建立一個動態陣列。大體上,你宣告這個陣列來說明它的存在(例如:在模組一開始的地方,你會去宣告一個陣列好讓模組中的所有程序都能用的到)用指令Dim來宣告,需要時再用ReDim來建立這個動態陣列。

' 在一般 模組中定義的陣列(Private)
Dim Customers() As String
 ...
 Sub Main()
     ' Here you create the array.
     ReDim Customer(1000) As String
 End Sub

如果你建立的是一個程序中局部性的陣列,你可以用一個簡單的ReDim指令來做:

Sub PrintReport()
     '這個陣列只地區性地建立此程序中,不被其他程序所使用
     ReDim Customers(1000) As String
     ' ...
 End Sub

如果你不指定陣列中的最低索引值,除非在模組一開始就有敘述,否則VB就假設其為0。筆者建議:絕對不用Option Base敘述,因為它會讓程式碼重覆使用的特性變的比較難。

(在剪貼項目時,你無法不去考量陣列的基底敘述)如果你要引用於零的最小索引值,用以下的語法來代替:

ReDim Customers(1 To 1000) As String

動態陣列可以任意的重新建立,而且每次都可以是不同數量。當你需要重新建立一個動態陣列時,它的容量就被重設回零(或空陣列)而陣列中所包含的資料也會遺失,如果你想要重新定義一個陣列大小而又不想放棄原有的資料,就用ReDim Preserve這個指令。

ReDim Preserve Customers(2000) As String

當你重設一個陣列大小時,你無法改變它的維度及它所含項目的型態。當你在多維度陣列使用ReDim Preserve這個指令時,你只能夠重設最後一個維度的大小。

ReDim Cells(1 To 100, 10) As Integer
 ...
ReDim Preserve Cells(1 To 100, 20) As Integer    ' 可作用
ReDim Preserve Cells(1 To 200, 20) As Integer    ' 不可作用

最後,你可以用Erase敘述句來刪除一個動態陣列。VB會釋放出這些元素所配置的記憶體空間(這時你就無法再讀寫它們了)如果是靜態陣列,其元素會被設為零或空字串。

你可以用Lbound和Ubound兩個變數來取回最小與最大索引值。如果陣列是二維或是更多維度時,你就需要傳遞第二個參數給這個函數來指定它們所需要的維度。

Print LBound(Cells, 1)    ' 顯示1, 第一維陣列的最小索引值
 Print LBound(Cells)      ' 同上
 Print UBound(Cells, 2)   '顯示1, 第一維陣列的最大索引值
 ' 計算元素總數
 NumEls = (UBound(Cells) _ LBound(Cells) + 1) * _
     (UBound(Cells, 2) _ LBound(Cells, 2) + 1)

UDTs中的陣列
 

UDT結構可以包含靜態和動態兩種陣列。這裡有一個包含兩種型態的結構。

Type MyUDT
     StaticArr(100) As Long
     DynamicArr() As Long
 End Type
 ...
 Dim udt As MyUDT
 ' 你必須DIMension動態陣列於使用前
 ReDim udt.DynamicArr(100) As Long
 '你不須DIMension靜態陣列於使用前
 udt.StaticArr(1) = 1234

靜態陣列所需的記憶體會在UDT結構中配置好;舉例來說,程式碼裡的靜態陣列實際只佔400個位元組。相反地,一個動態陣列在UDT中只佔了4個位元組,這四個位元組用來記錄資料在記憶區中存放位置的起始指標,當每一個UDT變數可能包含不一樣的數目的陣列時。在UDT使用動態陣列時,你不需事先宣告它的維度,不然會產生Error─9 "Subscript out of range." 的錯誤(指標超出範圍)

陣列與變數
 

VB允許你將陣列存放在Variant變數中,然後用Variant變數來存取其值就像陣列一樣。

ReDim Names(100) As String, var As Variant
 ' Initialize the Names array (omitted).
 var = Names()       ' 將陣列複製到變數中
 Print var(1)         ' 利用變數存取陣列項目

你甚至可以用Array函數快速建立一個Variant存放的陣列。

'陣列由Array()函數回傳,以0為基礎
 Factorials = Array(1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800)

同樣地,你可以傳遞一個陣列到程序中。

'此函數將陣列中所有值加總
 Function ArraySum(arr As Variant) As Variant
     Dim i As Long, result As Variant
     For i = LBound(arr) To UBound(arr)
         result = result + arr(i)
     Next
     ArraySum = result
 End Function

上面的程序最有趣的特性是它可以用在任何一個維度、型態的陣列。甚至可用於字串陣列,那樣就會得到所有字串相連的結果而不是所有值加總,這個程序相當地有用,它減少了處理不同型態陣列所需撰寫的程式碼。

但是你應該清楚,用Variant參數來存取陣列會很明顯地降低執行效率。如果你需要最好的效能就必須寫一個特定的程序來處理特定型態的陣列。你也可以傳遞一個多維陣列給需要的Variant參數的程序。如此,你仍可以由Variant變數來存取陣列的元素,但是如果你在程式編譯時不知道陣列的維度,你的程序中就必須先決定其值。

' 這個程序回傳當作參數處理的陣列維度
' 不是陣列時回傳0
 Function NumberOfDims(arr As Variant) As Integer
     Dim dummy as Long
     On Error Resume Next
     Do
         dummy = UBound(arr, NumberOfDims + 1)
         If Err Then Exit Do
         NumberOfDims = NumberOfDims + 1
     Loop
 End Function

小秘訣

在函數中使用函數名稱來當做區域性變數是完全合法的,如上一個例子,通常這個技法讓你在程序結束前同時存放區域變數及最後回傳的值,間接地加快一點執行時間。


以下是修飾過的ArraySum程序,使用了NumberOfDims而且適用於一維及二維陣列:

Function ArraySum2(arr As Variant) As Variant
     Dim i As Long, j As Long, result As Variant
     ' 先確定我們可否正確地操作這個陣列
     Select Case NumberOfDims(arr)
         Case 1       ' One-dimensional array
             For i = LBound(arr) To UBound(arr)
                 result = result + arr(i)
             Next
         Case 2       ' 二維陣列
             For i = LBound(arr) To UBound(arr)
                 For j = LBound(arr, 2) To UBound(arr, 2)
                     result = result + arr(i, j)
                 Next
             Next
         Case Else   ' 非陣列,或陣列維度超過
             Err.Raise 1001, , "Not an array or more than two dimensions"
     End Select
     ArraySum2 = result
 End Function

通常,如果一個Variant變數包含一個陣列,你無法事先知道這個陣列的基本型態。VarType函數回傳vbArray常數的總和十位數的8192,加上陣列中資料的VarType,讓你測試得到程序中的陣列型態:

If VarType(arr) = (vbArray + vbInteger) Then 
     ' 整數型陣列
 ElseIf VarType(arr) = (vbArray + vbLong) Then
     ' 長整數型陣列
 ElseIf VarType(arr) And vbArray Then
     ' 其它型態陣列
 End If

你也可以用IsArray函數來得知某個Variant是否包含陣列。當一個Varuant變數含有陣列時TypeName函數會增加一對括號在回傳的結果後。

Print TypeName(arr)      ' 顯示 "Integer()"

就像我說的,你可以指定一個陣列給一個Variant變數也可以將陣列當成參數傳給一個Variant變數,雖然這兩種運算看起來很類似,但實際上是不同的。VB實際上複製了一個陣列執行指定動作。因此,Variant變數並沒有指向到原來的資料而是指到複製的資料;從這點來看,所有對於Variant變數的動作並不影響到你原來的資料。相對的,如果你呼叫一個程序並且將陣列當作一個Variant參數傳遞,實際上並沒有任何資料被複製,Variant只是單純地向陣列的別名操作。你可以重新排列這個陣列的項目或異動它們的值,而且這些改變都會立即反應到你的原始資料。

陣列的指派與回傳
 

VB6新增了兩個重要的特性。首先,你可以在陣列與陣列之間完成指派的動作。再者,你可以寫一個回傳陣列的程序。你只能做指派相同型態的陣列,並且被指派的陣列必需是動態陣列。(因為VB可能會要重新定義目的陣列大小)

ReDim a(10, 10) As Integer
 Dim b() As Integer
 ' 填入資料給陣列  (省略)
 b() = a()        ' This works!

一般的指派指令永遠比一次複製一個項目For...Next相對應的迴圈要快上許多。而實際增加的速度大部份都仰賴陣列的資料型態。並且從20%到10倍不等。陣列間的一般指派也可以作用,在VB4及5你可以將一個陣列存放在一個Variant變數中,但是你無法反向操作─也就是說,你無法存取存放在Variant裡的陣列並將值回存至另一個特定型態的陣列。這在VB6中已解決。

Dim v As Variant, s(100) As String, t() As String
 ' 填滿陣列s()   (省略).
 v = s()        ' 指派給變數
 t() = v        ' 從變數指派動態字串陣列

我們會經常使用到陣列指派以建立陣列回傳函數的功能。注意下面程序中第一行最末的一對括弧;

Function InitArray(first As Long, Last As Long) As Long()
     ReDim result(first To Last) As Long
     Dim i As Long
     For i = first To Last
         result(i) = i
     Next
     InitArray = result
 End Function

新增的陣列性能讓你可以撰寫更多面性的陣列程序。VB6本身所包含一些新的字串函數叫做Join 、Split與Filter─就取決於此(詳細內容請參考 第五章 )。以下兩個範例你可以使用這新的特性來操作:

' 回傳長整數型態陣列的一部份值
' 注意:如果FIRST或LAST無效則失敗
 Function SubArray(arr() As Long, first As Long, last As Long, _
     newFirstIndex As Long) As Long()
     Dim i As Long
     ReDim result(newFirstIndex To last _ first + newFirstIndex) As Long
     For i = first To last 
         result(newFirstIndex + i - first) = arr(i)
     Next
     SubArray = result
 End Function
 ' 傳回一個包含ListBox上所有選項的陣列
 Function SelectedListItems(lst As ListBox) As String()
     Dim i As Long, j As Long
     ReDim result(0 To lst.SelCount) As String
     For i = 0 To lst.ListCount - 1
         If lst.Selected(i) Then
             j = j + 1
             result(j) = lst.List(i)
         End If
     Next
     SelectedListItems = result
 End Function

位元組陣列
 

VB的位元組陣列很特別,因為你可以直接指派陣列給它們,在這裡,VB直接複製字串內容的記憶位置,因為在VB5和VB6裡,所有的字串都是Unicode字串﹝每個字兩位元組﹞,所以被複製的陣列會被重新定義它的維度以得到實際字串的位元長度(你可以用LanB函數來取得)。如果字串包含它們的文字碼介在0到255之間的範圍(拉丁字母),則陣列中兩個位元都是0:

Dim b() As Byte, Text As String
 Text = "123"
 b() = Text      ' 現在陣列b() 包含6個項目:49 0 50 0 51 0

反向操作亦可行:

Text = b()

這個方法是為了簡化從舊有的VB3所使用的二元資料陣列所特別保留,如同前面〈位元組資料型態〉章節所解釋的。當你必須處理字串中每一個個別文字時,你可以利用這個特性來建立更快的程序。舉例來說,我們來看你可以用多快的速度來計算出字串中所有的空間。

'注意:本函數可能不適用於非拉丁語系字串
 Function CountSpaces(Text As String) As Long
     Dim b() As Byte, i As Long
     b() = Text
     For i = 0 To UBound(b) Step 2
         ' 只處理雙數項
         ' 用函數名稱當作區域變數節省時間和程式碼
         If b(i) = 32 Then CountSpaces = CountSpaces + 1
     Next
 End Function

我們一般用Asc及Mid$ 函數來處理參數中的文字,上面這段程序大約比這要快三倍。甚至如果你啟動Remove Array Bounds Check編譯最佳化,它還會更快,這個技巧唯一的缺點是它並沒有Unicode-friendly,因為它只考慮了二位元組和文字的最小位元。如果你計劃將你的應用程式轉成Unicode -如日文,你必須弄清楚這個最佳化的技巧。

陣列的插入及刪除
 

陣列中,我們最常用到的是插入及刪除項目,將剩餘項目索引值向上或是向下填補以釋放空間。通常你會用For...Next迴圈,甚至你可以寫一個適用所有型態陣列的通用程序來解決。(限制UDT陣列及不能下傳給Variant函數的固定字串)

Sub InsertArrayItem(arr As Variant, index As Long, newValue As Variant)
     Dim i As Long
     For i = UBound(arr) - 1 To index Step -1
         arr(i + 1) = arr(i)
     Next
     arr(index) = newValue
 End Sub
 Sub DeleteArrayItem(arr As Variant, index As Long)
     Dim i As Long
     For i = index To UBound(arr) - 1
         arr(i) = arr(i + 1)
     Next
     ' VB會將其轉成0或空字串
     arr(UBound(arr)) = Empty
 End Sub

如果你的應用程式密集地使用到陣列,你可能會發現建立在For...Next迴圈的處理效能太慢了,某些情況下,你可以適度地用RtlMoveMemory API函數來加快處理,即大部份程式設計師所知的CopyMenory這個函數讓你將一整個記憶區塊移到另一個記憶區塊的位置。甚至即使兩個區塊部份重疊了,它仍可成功作業。以下程式碼將插入一個新的項目到一長整數陣列:

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
     (dest As Any, source As Any, ByVal numBytes As Long)
 Sub InsertArrayItemLong(arr() As Long, index As Long, newValue As Long)
     ' We let VB evaluate the size of each item using LenB().
     If index < UBound(arr) Then
         CopyMemory arr(index + 1), arr(index), _
             (UBound(arr) _ index) * LenB(arr(index))
     End If
     arr(index) = newValue
 End Sub
 Sub DeleteArrayItemLong(arr() As Long, index As Long)
     If index < UBound(arr) Then
  CopyMemory arr(index), arr(index + 1), _
             (UBound(arr) _ index) * LenB(arr(index))
     End If
     arr(index) = Empty
 End Sub

注意

使用CopyMemory API函數的前提為資料必須存放於鄰近的記憶位址,如此你才絕對不會去用它來插入或移除字串和UDT物件陣列的元素。甚至是包含常規字串的陣列,動態物件參考陣列。(不過,固定長度字串和靜態陣列就沒有問題)


注意,雖然你無法使用以上的程序於長整數型態外的字串,程序中的主要敘述句還是可以利用Len B函數而被其他資料型態的字串所用而不需任何改變,你可以因此引申出新的陣列函數來處理其他型態的陣列,只需簡單的修改程序名稱及其接收參數列。例如,你只需異動第一行程式碼﹝如粗體所示﹞就能夠建立一個刪除雙精型態的陣列項目的函數:

Sub DeleteArrayItemDouble(arr() As Double, index As Long)
     ' 程式碼同函數DeleteArrayItemLong
     ' ...
End Sub

排序
 

排序是我們經常在陣列中會用到的,排序的方法有很多種,每一種都有其利弊。筆者發現Shell Sort運算法可適用於大部份。例如:筆者也列了一個通用程序,可以對任何一個一維陣列資料排序,其不論是升冪或降冪,並與Variant相容。

Sub ShellSortAny(arr As Variant, numEls As Long, descending As Boolean)
     Dim index As Long, index2 As Long, firstItem As Long
     Dim distance As Long, value As Variant
     ' 非陣列時離開
     If VarType(arr) < vbArray Then Exit Sub
     firstItem = LBound(arr)
     ' 找出distance最佳值
     Do
         distance = distance * 3 + 1
     Loop Until distance > numEls
     ' 陣列排序
     Do
         distance = distance \ 3
         For index = distance + firstItem To numEls + firstItem - 1
             value = arr(index)
             index2 = index
             Do While (arr(index2 - distance) > value) Xor descending
                 arr(index2) = arr(index2 - distance)
                 index2 = index2 - distance
                 If index2 - distance < firstItem Then Exit Do
             Loop
             arr(index2) = value
         Next
     Loop Until distance = 1
 End Sub

陣列中使用陣列
 

一個VB中的二維陣列,其結構並沒有什麼彈性,原因有二:(一)陣列中所有列所包含的數目必須一致;(二)你可以用ReDim Preserve來改變直欄數但確無法增加列數。第一點尤其重要,因為這常常會導致你無法去宣告一個大小遠比你所需還大得多的陣列,因此必須配置出許多用不到的記憶空間。這個問題你都能用陣列中使用陣列的結構來解決。

這個技巧很簡單,因為你可以存放一個陣列到變數中。所以你先建立一個Variant的陣列,而每一個子陣列 ─ 虛擬陣列 ─ 就可以包含不同多寡的項目數。你毋需使用太多的記憶空間。


 

這有一個範例,以一個假設的PIM程式為基礎,在這個程式中,必須持續一年中每一天的約會清單最簡單的解決方法是使用陣列中的列去對應一年中的每一天,與一直欄對應排定的約會﹝為了簡化,我們假設每一筆約會記錄為字串﹞:

ReDim apps(1 To 366, 1 To MAX_APPOINTMENTS) As String

當然你現在要設定MAX_APPOINTMENTS的常數值會是一個問題,你有足夠的資訊去大致估計一天內的約會數目,但卻會因此而浪費了許多實際上沒有用到的記憶空間。讓我們來看看不預設陣列大小而使用陣列中的陣列技巧可以節省多少記憶空間:

' 模組層級變數
Dim apps(1 To 366) As Variant

' 加入約會項目到特定一天
Sub AddNewAppointment(day As Integer, description As String)
    Dim arr As Variant
    If IsEmpty(apps(day)) Then
        ' This is the first appointment for this day.  
        apps(day) = Array(description)
    Else
        ' 加入約會項目到已經排定的約會中
        arr = apps(day)
        ReDim Preserve arr(0 To UBound(arr) + 1) As Variant
        arr(UBound(arr)) = description
        apps(day) = arr
    End If
End Sub
' 追蹤特定一天的所有約會
Sub ListAppointments(day As Integer, lst As ListBox)
    Dim i As Long
    For i = 0 To UBound(apps(1))
        lst.AddItem apps(1)(i)
    Next
End Sub

在這個例子裡,筆者儘量簡化了程式碼並且在陣列中使用Variant陣列。如果你能明確地定義陣列中每一列的資料型態(如本例中的字串型態),你能節省的記憶空間又更多。注意下面這個特殊的語法用來指定陣列中的陣列。

' 改變第n個約會項目說明
apps(day)(n) = newDescription

你可以無限延伸這個觀念,引用陣列中的陣列,以此類推。如果你要處理的陣列每一列中包含的資料長度不等,你就可以用這個方法來節省記憶空間,甚至是整個應用程式的執行效率。陣列中使用陣列的最大特色為你可以將虛擬陣列(pseudo-array)中一整列的資料當作單獨項目處理。例如,替換、取代、新增、或刪除它們。

' 將1月1日的約會項目移至到1月2日
apps(2) = apps(1)
apps(1) = Empty

最後,這個技巧的一個重要好處是你可以在不改變陣列中目前內容的情況下去新增一列新的陣列。(記得,你只能在一般陣列中使用ReDim Preserve去異動直欄數,卻不能改變其列數。)

' 擴充一年份(非潤年)的行事曆
ReDim Preserve apps(1 to UBound(apps) + 365) As Variant

Collections集合物件
 

Collection集合物件是放在VBA的程式館,可以用在Visual Basic應用程式中存放整組相關資料。類似於我們對陣列的了解,但實質上卻有下列幾點的不同:

Collection物件有這麼多優點,也許你會想為什麼它在Visual Basic開發核心中沒有取代掉陣列。主要原因是Collection物件效率較慢,至少很明顯地比陣列慢。例如:填寫10,000個長整數元素到陣列中就比到Collection物件要快上100倍。當你考量哪一種資料型態較能解決你的問題時,這點就必須考慮到。

使用Collection物件前你必須先建立好它,如同其它物件,必須先宣告然後建立:

Dim EmployeeNames As Collection
Set EmployeeNames = New Collection

或者,你可以用以下程式碼宣告一個自動定義的集合物件:

Dim EmployeeNames As New Collection

你可以用Collection物件的Add方法來增加項目;傳遞項目的值以及其關聯鍵字串:

EmployeeNames.Add "John Smith", "Marketing"

項目的值可以是任何能被存放在Variant變數的值。Add方法通常會將值新加在Collection物件最後,但是你也可以用before或after引數明確指定其存放的位置。

' 在Collection物件最前端插入
EmployeeNames.Add "Anne Lipton", "Sales"
' 在上個增加的項目後插入新值
EmployeeNames.Add value2, "Robert Douglas", ,"Sales"

除非你有特殊理由必須在其他位置插入新值,不然我建議你不要用before或after這兩個引數,因為它們會使得Add方法速度變慢。選擇性的使用其關聯鍵字串,如果你指定了一個已經存在的關聯鍵字串,Add方法會產生錯誤訊息error 457-"This key is already associated with an element of this collection." (鍵值比對有大小寫之分)

一旦你增加了一個以上的值,你就可以用Item方法來取出其值;這個方法是Collection類別的內定成員,所以你可以省略它。Collection物件內的項目可以用索引值或關聯鍵字串來讀取:

' 以下所有敘述句列印出 "Anne Lipton"
Print EmployeeNames.Item("Sales")     
Print EmployeeNames.Item(1)
Print EmployeeNames("Sales")
Print EmployeeNames(1)

小秘訣

如果你希望你的程式執行得比較快,最好是用關聯鍵字串來存取Collection物件內的項目而不是用它的索引值,尤其當項目有上千個而你要存取的值並非位於物件開始處。


如果你傳遞的索引值為負數或大於目前物件中的項目數時,錯誤訊息會產生error 9-"Subscript out of range" (完全跟你在處理陣列時一樣);如果傳遞的關聯鍵字串不存在,也會有錯誤訊息error 5-"Invalid procedure call or argument." 令人感到好奇的是,Collection物件沒有檢驗項目是否存在的內建方法,唯一的辦法是設定錯誤回報處理器來測試。你可以使用下面這個函數:

Function ItemExists(col As Collection, Key As String) As Boolean
    Dim dummy As Variant
    On Error Resume Next
    dummy = col.Item(Key)
    ItemExists = (Err <> 5)
End Function

函數Count回傳Collection物件所包含的項目數:

' 取出Collection物件EmployeeNames最後一個項目的值
' 注意,Collection物件以1為基礎
Print EmployeeNames.Item(EmployeeNames.Count)

你可以利用Remove方法移除物件中的項目;這個方法接受項目的索引值及關聯鍵字串:

' 移除項目Marketing Boss
EmployeeNames.Remove "Marketing"

如果項目不存在,則產生錯誤訊息error 5-"Invalid procedure call or argument." Collection物件不提供內建方法來移除單一項目,所以你得被迫寫一個迴圈來處理。這裡寫好了一個通用函數:

Sub RemoveAllItems(col As Collection)
    Do While col.Count
        col.Remove 1
    Loop
End Sub

小秘訣

移除Collection物件內的所有項目最快的方法是砍掉集合物件本身,直接設其等於0或讓它等於一個全新的值。

' 以下兩行都會將目前Collection物件裡的全部內容移除
Set EmployeeNames = Nothing
Set EmployeeNames = New Collection

但是,這個程序只作用於Collection物件沒有被其它物件變數所指向時。如果你不是很確定的話,唯一安全的作法是使用之前提到的迴圈。


最後,如同之前所述,Collection物件不允許你修改所含項目的值。如果你想改變其值的話,你必須先刪除原來項目再新增進來。參考以下程序:

' INDEX可以是數值也可以是字串
Sub ReplaceItem(col As Collection, index As Variant, newValue As Variant)
    ' 先將項目移除(項目不存在則跳出)
    col.Remove index
    ' 再將項目新增回來
    If VarType(index) = vbString Then
        ' 加入一個相同關聯鍵名稱的項目
        col.Add newValue, index
    Else
        ' 加入項目到原來位置(沒有鍵值)
        col.Add newValue, , index
    End If
End Sub

Collections集合物件的重複操作(Iterating)
 

因為我們可以利用索引值來指向Collection物件中的所有項目,所以可以使用一般的For...Next迴圈操作:

' 將Collection物件中所有項目填入一個ListBox控制項
Dim i As Long
For i = 1 To EmployeeNames.Count
    List1.AddItem EmployeeNames(i)
Next

雖然上面這個程式碼可以作用,Collection物件提供另一個更好的方式來完成作業,利用For Each...Next迴圈:

Dim var As Variant
For Each var in EmployeeNames
    List1.AddItem var
Next

注意控制迴圈的變數(本例中的var)必須是Variant型態,如此它才能操作任何型態的資料。唯一例外的情形是你確定Collection物件只包含(表單、控制項、或使用者自訂物件)這些類別物件。你可以使用這些特定物件型態的控制變數:

' 假設Customers集合只包含個別Customer物件的參照名稱(references)
Customer objects
Dim cust As Customer
For Each cust In Customers
    List1.AddItem cust.Name
Next

使用特定物件型態的控制變數(controlling variable)比起通用Variant或Object變數提供較佳的效能。使用For Each...Next迴圈重複操作於某個集合的所有元素比使用一般的For...Next迴圈普遍上快很多,因為後者強迫你使用項目的索引值對其作指向,相對地拖慢了整個操作。

Collections集合物件的處理
 

Collection物件的結構非常有彈性,它可以解決一些簡單但不斷重複的程式問題。任何時候需要用到關聯鍵來快速取出項目的值時,你都可以利用到它的特性。以下程序建立在Collection物件只接受唯一關聯鍵的條件下,用以過濾出某個與Variant型態相容(Variant-compatible) 的陣列中所有重複的輸入值:

' 過濾出陣列中所有重複的輸入項
' 運算開始時,NUMELS必須設成檢驗項目的總數
' 運算結束時,NUMELS包含非重複項目的總數
Sub FilterDuplicates(arr As Variant, numEls As Long)
    Dim col As New Collection, i As Long, j As Long
    On Error Resume Next
    j = LBound(arr) - 1
    For i = LBound(arr) To numEls
        ' 加入一個虛擬數0並且使用陣列的值當成加入項目的關聯鍵名稱
        col.Add 0, CStr(arr(i))
        If Err = 0 Then
            j = j + 1
            If i <> j Then arr(j) = arr(i)
        Else
            Err.Clear
        End If
    Next
    ' 清除剩餘項目
    For i = j + 1 To numEls: arr(i) = Empty: Next
    numEls = j
End Sub

在這個例子裡,你也許會覺得受限於Collection物件不能包含UDT值而不知道該如何儲存多個相同關聯鍵的項目。解決辦法之一是使用物件(objects)而非UDT,但是這樣又有點殺雞用牛刀,因為你不太可能為了只是要在Collection物件裡存放重複的值而去包含一個類別物件(class object)在你的Project中。另外一個較好的解決辦法為快速建立幾個陣列來當作儲存到Collection物件的項目。實際應用如下:

' 儲存Employees資料到Collection中
Dim Employees As New Collection
' 每一個項目由(Name, Dept, Salary)組成
Employees.Add Array("John", "Marketing", 80000), "John"
Employees.Add Array("Anne", "Sales", 75000), "Anne"
Employees.Add Array("Robert", "Administration", 70000), "Robert"
...

' 列出所有員工姓名
Dim var As Variant
For Each var in Employees
    Print var(0)       ' 項目0為員工姓名
Next
' 列出Anne住哪
Print Employees("Anne")(1)
' 列出Robert賺多少錢
Print Employees("Robert")(2)

當然,你可以讓這個複合物件依照你的需求變得十分複雜。例如,每一個Employees 元素可能再包含一個其它資訊的Collection,像是每一個員工為特定客戶服務多少小時等:

Dim Employees As New Collection, Customers As Collection
' 每一個項目由(Name, Dept, Salary, Customers)組成
Set Customers = New Collection
Customers.Add 10, "Tech Eight, Inc"
Customers.Add 22, "HT Computers"
Employees.Add Array("John", "Marketing", 80000, Customers), "John"
' 每次都開始於一個新的集合
Set Customers = New Collection
Customers.Add 9, "Tech Eight, Inc"
Customers.Add 44, "Motors Unlimited"
Employees.Add Array("Anne", "Sales", 75000, Customers), "Anne"
' 以此類推...

這個複合結構讓你快速地解決了部份問題,不過它也引發了某些有趣的問題:

' 解答John是否服務於HT Comuters
Dim hours As Long, var As Variant
On Error Resume Next
hours = Employees("John")(3)("HT Computers")
' 上述說明不為真時則HOURS的值為0

' 解答Anne服務了多少家客戶
hours = 0 
For Each var In Employees("Anne")(3)
    hours = hours + var
Next
' 解答投入在Tech Eight, Inc投入的時間
On Error Resume Next
hours = 0
For Each var In Employees
    hours = hours + var(3)("Tech Eight, Inc")
Next

如你所見,集合物件在資料結構上有高度的彈性,如果你深入探討它的效能,我相信你經常會用得到它。

Dictionary物件
 

Dictionary物件對Visual Basic語言來說是新的東西,然而,技術性上它並不像Collection物件一樣屬於Visual Basic,它也不屬於VBA語言,它甚至是放在Microsoft Scripting Library外部程式館裡。事實上,要使用這個物件之前,你必須先把這個參照表加入到SCRRUN.DLL程式館(其顯示名稱為Microsoft Scripting Runtime, 如圖4-1)。一旦你加入後,就可以按[F2]鍵來啟動物件瀏覽器(Object Browser)去探究Dictionary物件的方法及程序。

Dictionary物件跟Collection物件非常相像,實際上,它們原本就都是為了提供VBScript程式設計師一個類似集合的物件(Collection-like)而設計的。Dictionary物件並不專屬於Visual Basic 6;Scripting Library可以從網站 http://msdn.microsoft.com/scripting/ 免費下載並使用於任何自動控制化(Automation-compliant)程式語言,包括Visual Basic 5。Visual Basic 6將這個程式館設置成安裝程序的一部份,所以你不須要去下載再分別註冊。


 

圖4-1 在References對話視窗增加一個參照物件(reference)到Microsoft Scripting Runtime library以使用Dictionary物件

你馬上就能發現Dictionary物件跟Collection物件有多麼得相似,所以要比較這兩者之間的特性把它們列舉出來是相當容易的。建立一個Dictionary物件像你建立任何一個物件一樣,如使用自動定義變數(auto-instancing):

Dim dict As New Scripting.Dictionary

注意所有情況下Scripting字首都可以省略,但我還是建議你使用以免你將來可能會在參照表中用到一些有Dictionary物件的外部程式館。使用完整的libraryname.classname語法是避免將來可能發生錯誤的聰明途徑。


注意

VBScript導向的Dictionary物件有點抗拒轉移到Visual Basic。Visual Basic 6手冊的所有例子都從VBScript文件來,因此,使用CreateObject函數來建立Dictionary物件(VBScript不支援New運算符號)。此外,所有例子都用Variant變數型態儲存參照(references)到Dictionary物件(VBScript不支援特殊物件變數)。

' VB6處理報表的方法
Dim dict         ' Variant是VB的預設資料型態
Set dict = CreateObject("Scripting.Library")

雖然這個程式碼可行,你還是要絕對避免使用它,有兩個原因: CreateObject的執行時間大約是New的兩倍,更重要的是,不使用特定型態的變數而使用Variant變數增加了你每一次存取物件屬性及方法的管理負擔,因為你實際上是用了新的資料結構而不是早先的。筆者一些非正式的查驗程序顯示出使用特定型態變數比起通用Variant變數快了約30倍,這樣也可以寫出一個較穩定強韌的應用程式,因為所有的語法錯誤都是編譯器設的陷阱。


利用Dictionary物件的Add方法來新增項目,就像在Collection物件一樣,但是兩個參數的傳遞順序是相反的,並且不能省略關聯鍵名稱,也不能加上before或after兩個引數:

dict.Add "key", value

如果Dictionary物件包含了一個值其關聯鍵名稱已經存在,會產生錯誤訊息error 457(與Collection物件一樣)。Dictionary物件支援Item成員,但與Collection物件的Item成員有很大的不同,Collection的Item是完成(implenmented)成員,至於Dictionary物件,Item是一個讀寫(read-write)屬性而非方法,你只能用其關聯鍵(可以是字串也可以是數字)來讀取元件,卻不能用它的數值索引。換句話說,你只能用關聯鍵存取項目而非項目位址:

Print dict("key")             ' 列出目前的值
dict("key") = newValue       ' 修改這個值
Print dict(1)                ' 因為這個關聯鍵所指項目沒有值
                          '所以顯示一個空字串

這兩個物件還有第三點不同:如果在Dictionary裡找不到某個鍵名,並不會有錯誤訊息產生,如果你試著要去讀取那個項目的值,Dictionary會回傳一個空值;如果此時你指派了一個值,Dictionary就會新增一個項目。換句話說,你不必用到Add方法就可以新增一個項目:

Print dict("key2")            ' 回傳空值
dict(key2) = "new value"      ' 增加一個新項目並且不會產生錯誤

關於這點,Dictionary物件就比較接近PERL的相關陣列而不同於Visual Basic的Collecytion物件。跟Collection一樣,Dictionary支援Count屬性,但是你無法利用它來設置For...Next迴圈。

你可以利用Remove方法來移除Dictionary項目:

dict.Remove "key"           ' 不支援數值索引

找不到鍵名時,會產生錯誤訊息error 32811-"Method 'Remove' of object 'IDictionary' failed"(這個訊息對實際發生原因沒有什麼幫助)。不同於Collection,你可以用RemoveAll方法一次將全部項目移除:

dict.RemoveAll          ' 不須使用迴圈

Dictionary物件也比Collection物件更有彈性,你可以利用Key屬性修改關聯項目的鍵名:

dict.Key("key") = "new key"

Key為唯寫(write-only)屬性,但這不是實際上的限制:嘗試讀取其值並不合理,因為你只會用它來對照項目。Dictionary物件陳列了一個Exists方法讓你可以檢驗項目是否存在。你會非常需要它,不然你無從判別不存在的項目與包含空值的項目:

If dict.Exists("John") Then Print "Item ""John"" exists"

Dictionary物件亦陳列了另外兩個方法,Items與Keys,你可以用來快速取出所有項目的值及關聯鍵到陣列中:

Dim itemValues() As Variant, itemKeys() As Variant, i As Long
itemValues = dict.Items    ' 取出所有值
itemKeys = dict.Keys       ' 取出所有鍵值
' 將這些值及關聯鍵列在一個目錄內
For i = 0 To UBound(itemValues)
    List1.AddItem itemKeys(i) & " = " & itemValues(i)
Next

Items與Keys方法也是存取Dictionary物件元素的唯一方法,因為你既無法利用For...Next迴圈(因為數值索引值也被視為項目的關聯鍵名稱)也無法利用For Each...Next迴圈。如果你不希望匯出項目到Variant陣列,你可以利用下面的捷徑,Variant陣列支援For Each...Next迴圈:

Dim key As Variant
For Each key In dict.Keys
    List1.AddItem key & " = " & dict(key)
Next

有趣的是,Keys是Dictionary物件的預設方法,所以之前的程式碼中你可以省略它,讓這段程式語法看起來就好像Dictionary物件是支援For Each...Next迴圈一樣:

For Each key In dict
    List1.AddItem key & " = " & dict(key)
Next

Dictionary物件的最後一個特性是它的CompareMode屬性,它說明了Dictionary物件如何比對鍵值,你可以指定3個值給它:0-BinaryCompare (預設值,有大小寫之分case-sensitive),1-TextCompare (case-sensitive),及2-DatabaseCompare (Visual Basic不支援)。你只能在Dictionary物件為空時指定這個值。

Dictionary物件vs. Collection物件
 

很明顯地Dictionary物件比Collection物件更有彈性,唯一不足的地方是無法用索引值來指定項目(也就是在Collection中最慢的運算處理),除非你不考慮效能,不然要選擇哪一個物件已經很清楚了:在任何需要較大彈性的時候使用Dictionary物件。但是記得你的應用程式必須分散配置額外的輔助檔案。

Microsoft並未顯示Collection和Dictionary物件內部實行的方式,但對我而言似乎Dictionary是以比Collection物件更有效率的演算法做為基礎。筆者的非正式基準顯示建立一個10,000個項目的Dictionary的速度,約為在一個空白Collection物件建立相同數量項目的速度快上7倍;將這些項目讀的速度會快上3到4倍。當你建立較大的結構時,這個差異會減少(只有100,000個項目的2.5倍),但一般而言Dictionary物件仍被認為並Collection物件快。實際速度的差異可能在於可用記憶體等因素的影響。在你將一個解決方案或其他方案最佳化之前,建議你使用你自己真正的資料建立一些基準。

程序
 

Visual Basic模組是由宣告區─宣告模組中使用的資料型態、常數及變數─ 加上程序組成,這些程序可以是Sub或Function的型態,取決於該程序是否回傳值,它也可以是Property屬性程序,我們會在 第六章 討論。每一個程序有其獨一的名稱、參數列、及回傳值──如果是Function函數。

範圍
 

程序的範圍﹝Scope﹞可以是私有的Private、公用的Public、或Friend。Private程序定義只能在它的程序內使用,Public程序則可以被引用於模組之外。如果本身為Public(其屬性Instancing不等於1-Private而且包含於非標準 .EXE型態的模組中),其程序可以透過COM被外部呼叫,因為Public是程序的預設型態,所以可以省略。

' Public函數,用於存取表單中的控制項
Function GetTotal() As Currency
    GetTotal = CCur(txtTotal.Text)
End Function

如果範圍不是Public你必須明確的指定:

' 所有的事件﹝event﹞,都是Private
Private Sub Form_Load()
    txtTotal.Text = ""
End Sub

Friend程序的作用範圍介於Private和Public之間:其程序可以引用於Project內的任何地方,但卻不能被外部呼叫。這個差異只有在Project本身不是標準 .EXE型態時才顯的比較重要,因此我們以COM的型式來陳列它的類別給其他的應用程式。筆者將在 第十六章 深入討論COM元件,在此先說明一些主要的概念。

為了瞭解Friend的好用,想像一下以下的情節:你現在有一個Public類別模組,顯示一個要求使用者輸入名稱及密碼的對話框,和一個GetPassword函數供Project內的其他模組使用;以確認密碼判別特定函數是否可供使用者使用。那你應該將這個函數宣告為Private嗎?不,因為它可能會被其他模組所引用,那應該將它宣告為Public嗎?也不對,因為這樣可能會讓一些惡意的程式設計者從外部Project去查詢你的模組,進而竊取使用者密碼(為了簡便,讓我們假設從你的類別取得資訊沒有問題),在這情況下,最好的選擇就是將函數定義為Friend。


 

如果你是在一個標準EXE 的Project裡,或是在任何型態Project的類別裡,Public性質就相等,這裡的程序無論如何都不能從外部呼叫。

接收參數與回傳值
 

Sub與Function程序都可以接收參數,Function亦可以傳回值。設定合理的接收參數列與回傳值是使程序能更好用的原因。你也可以將參數宣告為Object、Collection程式中所定義的類別、或外部型態(如:Dictionary物件)至於上述這些資料型態的陣列回傳值也是一樣的。(Visual Basic 6新增的特性)

你應該清楚Argument是傳遞給程序的值,而Parameter是接收的值。這兩者實際上都指向相同的一個值,到底哪一個才是最適當的用辭則取決於你從哪一個觀點來看待:呼叫程序的是Argument,而被呼叫的程序是Parameter。這一節,Argument及Parameter在某種程度上可以交替使用。

傳值或參考傳遞
 

參數的傳遞可以利用值(Byval)或參考(ByRef),後者可以被呼叫程序修改,修改後的值還可以被呼叫者讀回,而前者的值如果在呼叫程序中異動的話,其改變並不影響呼叫者。你要遵循的規則為「當值須要在程序中作異動時,永遠傳遞它的參考(reference),不然就傳遞本身」。這樣可以減少異動傳遞變數值的錯誤。

讓筆者用下面的例子說明:

' Z被錯誤地用ByRef宣告
Sub DrawPoint(ByVal X As Long, ByVal Y As Long, Z As Long)
    ' 保持參數為正數
    If X < 0 Then X = 0
    If Y < 0 Then Y = 0
    If Z < 0 Then Z = 0      ' 可能導致錯誤!
End Sub

這個程序修改接收的值以使它們在有效範圍內;如果接收參數是用Byref來傳遞,如上例中的z,這些改變將影響呼叫的程式碼。這類錯誤需要一些時間來發現,尤其當你大部份時候都用常數或運算式來呼叫程序。

' 這樣可以作用(參數為一個常數)
DrawPoint 10, 20, 40       
' 這也可以作用(參數為一運算式)
DrawPoint x * 2, y * 2, z * 2  
' 這也可以作用,但它異動z的值(z為負值時)
DrawPoint x, y, z

利用ByVal宣告接收參數提供了另一個好處,你可以傳遞任何型態的變數或運算式給呼叫程序,然後該Visual Basic幫你轉換資料型態。相對地,如果接收參數利用ByRef來宣告,而且你傳遞的是一個變數,則它們的型態必須一致:

' 假設x,y,z非長整數變數(例如:Single或Double)
DrawPoint x, y, 100   ' This works, Visual Basic does the conversion.
DrawPoint x, y, z     ' This doesn't. (ByRef argument type mismatch.)

然而上述規則有一點例外,如果程序接收的參數是以ByRef Variant陳列,任何型態的資料你都可以傳遞給它。你可以利用這個特性撰寫一個不指定資料型態的程序,如:

' 交換任何型態的值
Sub Swap(first As Variant, second As Variant)
    Dim temp As Variant
    temp = first: first = second: second = temp
End Sub

使用ByVal還有一個小細節,當程序使用二個或多個不同的方法存取記憶體中同一位址時,例如:一個共用模組級變數被當作參數傳遞時,這個變數在程序中就被賦予一個別名(Alias)。變數別名的問題使VB編譯器無法最佳化這些程式碼。當所有變數都以值來傳遞給程序及方法時,就無法從其中任何一個程序的接收參數異動公用變數的值,編譯器可以產生較佳化的碼。

如果你相當確定你程式所有程序都符合這個限制,VB內定的編譯器會最佳化你的程式碼,開啟Project-Properties對話框,切換到Compile標籤頁,點選進階最佳化Advanced Optimizations鈕,勾取Assume No Aliasing選項,如圖4-2,以通知VB程式中不含任何別名變數。


 

圖4-2 Advanced Optimizations對話框

傳遞使用者自定型態資料
 

你也許注意到筆者沒有提及UDT結構在程序中的接收與回傳。事實上,這類型的結構並非都可以被當參數來使用的,考慮下列狀況:

你甚至無法在非Public的類別模組裡宣告一個Public UDT結構,於是就不能宣告一個Public UDT於一般模組外的標準EXE Project。


注意

如果你要建立一個Microsoft ActiveX EXE project,就要清楚UDT值只能跨於DCOM98(Windows 9 x)或Service Pace 4(Windows NT 4.0)的系統,否則當Visual Basic嘗試傳遞一個UDT值給其它程序時就會產生錯誤error 458─"Variable uses an Automation Type not supported in Visual Basic"。你和使用者的作業系統兩者都需要更新(update)。

注意,這個問題點並不套用於ActiveX DLL project,因為ActiveX DLL與呼叫它的程式共享同樣的記憶位址,所以可以不需COM來傳遞UDTs。


傳遞Private型態資料
 

傳遞Private物件給程序有許多限制,其中Private Object定義於你的應用程式中不能被外部使用。Private物件定義於Instancing屬性等於1-Private的類別,或Visual Basic程式館陳列的物件裡,包括表單(Form)、控制項(Controls)、及物件(Object)如:App、Clipboard、Screen、與Printer物件。大至上,如果程序會被外部應用程式透過COM來呼叫,你的程序就不能用這些Private物件來當做參數,這些物件也不能當做函數的回傳值。這限制是十分合理的,因為COM元件在應用程式之間是當資訊交換的媒介,可以處理任何VB支援的基本資料型態以及Windows環境下程式所定義的物件。換言之,COM元件無法傳遞程序中定義的資訊格式,如下面程式碼中的Private類別。

' 如果程序位於一個Public類別中,VB不會邊譯下面幾行
Public Sub ClearField(frm As Form) 
...
End Sub

如果方法(method)被宣告為Private或Friend,這個限制就不會被執行,因為這些方法不會被外部應用程式透過COM來呼叫,它只能被自己應用程式中其他模組使用。這種情況下就不需要限制傳遞給方法的資料型態為何了,而且事情上VB編譯器也不會去管Private資料型態是否被當做參數傳遞或方法(method)的回傳值。

' 編譯時不會有問題,甚至是在Public類別裡也一樣
Friend Sub ClearField(frm As Form) 
...
End Sub

說明

這裡有一個簡單的辦法解決Private物件傳遞到程序的限制,只需用As Object或As Variant來宣告程序的接收或回傳值:如此,編譯器就不會知道實際上哪一個物件會在執行時期被傳遞,也就不會在那一行標幟錯誤。雖然這個技巧可行,你還是得知道Microsoft強烈反對並公開說明這個作法將來可能失效。


Optional關鍵字
 

Visual Basic 4就已經可以使用Optional關鍵字於程序接收參數列,這個可選擇性的參數必須置於一般(必需)參數之後,VB 4只支援Variant型態的選擇性接收參數,並允許利用IsMissing函數檢驗參數是否存在:

' 表單內的Public method
Sub PrintData1(text As String, Optional color As Variant)
    If IsMissing(color) Then color = vbWhite
    ForeColor = color
    Print text
End Sub

使用IsMissing函數時要非常小心,因為如果你指派了一個值給遺失的參數,這個函數會從那一點上傳回False,讓我們看看引用這個函數為什麼沒有預期的效果:

Sub PrintData2(text As String, Optional color As Variant)
    Dim saveColor As Long
    If IsMissing(color) Then 
        Form1.FontTransparent = False
        color = vbWhite
    End If
    Form1.ForeColor = color
    Form1.Print text
    If IsMissing(color) Then 
        ' 接下來的敘述句將不被執行
        Form1.FontTransparent = False
    End If
End Sub

Visual Basic 5增加了使用選擇性參數的可行性-不只支援Variant-而且將它們設在程序的預設接收參數內。PrintData1程序在Visual Basic 5和Visual Basic 6裡可以寫得更簡潔:

Sub PrintData3(text As String, Optional color As Long = vbWhite)
    Form1.ForeColor = color
    Form1.Print text
End Sub

注意

如果選擇性參數為Variant之外的型態,函數IsMissing永遠回傳False,這樣可能會造成許多小錯誤,如下面的程式碼:

Sub PrintData4(text As String, Optional color As Long)
        If IsMissing(color) Then
            ' The next line will never be executed!
           Form1.FontTransparent = False
       End If
       ' ...
End Sub

當一個非Variant型態的選擇性參數在接收列沒有設立其指定型態,程序將接收一個等於0的值、空字串、或者Null,取決於接收參數的型態。你唯一不能用作選擇性參數的型態是UDT結構。

Optional參數有助於撰寫較具彈性的程序,但與一些程式設計師的認知相反,它並不產生較有效率的程式碼,這個假設為:因為呼叫程序不須將其存到堆疊中,如此較少的CPU敘述句被執行,所以程式運作得更快。很不幸地,這個假設並不成立,當我們省略掉選擇性參數時,Visual Basic實際上堆疊了一個特別的"missing"值,所以並沒有節省到任何一點執行時間。

這個Visual Basic所編譯出的"missing"值即錯誤碼&H80020004。函數IsMissing除了檢驗並回傳參數是否存在外不作任何事,附帶解釋一下為什麼它總是對Variant型態以外的資料回傳False:所有資料型態只有Variant變數可以保存錯誤碼(Error value)。你無法直接建立這個特殊的值因為CVErr函數只接受0到65,535的範圍,不過你可以利用下面的小技巧:

' 呼叫這個函數時永遠省略其參數
Function MissingValue(Optional DontPassThis As Variant) As Variant
    MissingValue = DontPassThis
End Function

參數命名
 

雖然Optional參數是VBA語言裡一個相當好用的新增功能,但它卻減少了程式碼的可讀性,舉例來說:

Err.Raise 999, , , "Value out range"

這看起來就像程式的錯誤;它有太多的逗號,其Value out range字串落於HelpFile領域,有多少程式設計師能夠單單瀏覽程式原始碼就能認出這個錯誤來?很幸運地,當你呼叫程序時,可以利用參數命名來減低選擇性參數的不利影響。這裡將上面那個例子作了一些修正:

Err.Raise Number:=999, Description:="Value out of range"

參數命名讓你可以改變呼叫程序傳遞參數的順序,但是並不允許你省略非選擇性參數。你在Visual Basic建立的所有程序都自動支援命名的參數。例如:

Sub Init(Optional Name As String, Optional DeptID As Integer, _
    Optional Salary As Currency)
    ' ...
End Sub

你可以用下面這個方法呼叫:

Init Name:="Roscoe Powell", Salary:=80000

ParamArray關鍵字
 

你可以利用ParamArray關鍵字寫一個接受任何多寡參數的程序:

Function Sum(ParamArray args() As Variant) As Double
    Dim i As Integer
    ' ParamArrays全都以0為基礎
    For i = 0 To UBound(args)
        Sum = Sum + args(i)
    Next
End Function

用下列方法呼叫Sum函數:

Print Sum(10, 30, 20)  ' 顯示60

幾個簡單的規則說明ParamArray關鍵字如何作用:

ParamArray關鍵字在建立通用函數時可能沒什麼幫助,舉例來說,你可以建立一個函數來回傳數目的最大值:

Function Max(first As Variant, ParamArray args() As Variant) As Variant
    Dim i As Integer
    Max = first
    For i = 0 To UBound(args)
        If args(i) > Max Then Max = args(i)
    Next
End Function

注意上面這個程序只有一個必需參數,因為求出的最大值為0並不合理,雖然沒有文件說明,你還是可以用args( ) 當作IsMissing函數的參數,所以你有兩個方法離開沒有任何選擇性參數傳遞進去的程序:

' 文件說明的方法
If LBound(args) > UBound(args) Then Exit Function
' 文件沒有說明的方法
If IsMissing(args) Then Exit Function

ParamArray關鍵字也可以用來回傳陣列,雖說Array函數讓你可以快速建立一個Variant型態的陣列,VBA卻不提供相似的功能給其他型態的陣列。下面這個方法可以補救這個問題:

Function ArrayLong(ParamArray args() As Variant) As Long()
    Dim numEls As Long, i As Long
    numEls = UBound(args) _ LBound(args) + 1
    If numEls <= 0 Then Err.Raise 5     ' 無效的程序呼叫
    ReDim result(0 To numEls - 1) As Long
    For i = 0 To numEls _ 1
        result(i) = args(i)
    Next
    ArrayLong = result
End Function

ParamArray關鍵字的最後一點說明:如果你想要最好的執行效能,就得仔細看看這段。ParamArray關鍵字強迫你使用Variant接收參數,Variant是Visual Basic支援的資料型態中最慢的,所以如果你必須用到選擇性參數,使用非Variant的Optional參數會加快許多。

錯誤處理器
 

錯誤處理是VB語言很重要的一個特性,與你的程序結構緊密結合,Visual Basic提供3個指令讓你管理執行時期發生的錯誤:

依你的程式風格及需求選擇要用哪一個錯誤處理指令,這裡沒有什麼規則,所有On Error令都能有效地清除目前錯誤碼。

On Error Goto <label> 指令
 

處理檔案時,On Error Goto <label> 指令通常是較好的選擇,因為這種狀況下,錯誤可能有很多,而你又不想一個指令一個指令地測試錯誤碼。同樣的觀念也應用於容易出錯的數學程序,如除數等於0,溢位,非法呼叫函數。大部份狀況下,發生錯誤時最佳的處理方法是迅速離開程序,然後回報錯誤。

換言之,也有許多非嚴重錯誤(fatal error)的情形。假設你要求使用者放入磁片到A槽,當他們放入的磁片不正確時,你希望讓他們有第二次機會來更換,而不是錯誤一發生就中斷整個程序的執行,這個程序幫你檢查磁片是否正確放入:

Function CheckDisk(ByVal Drive As String, VolumeLabel As String)_
     As Boolean
     Dim saveDir As String, answer As Integer
     On Error GoTo Error_Handler
     Drive = Left$(Drive, 1)
     ' 儲存目前磁碟機供後來存取
     saveDir = CurDir$
     ' 用Next指令解除錯誤statement is likely to fire an error.
     ' 檢查指定於參數中的的磁碟機
     ChDrive Drive
     ' 磁碟代號相符時回傳True,反之回傳False
     CheckDisk = (StrComp(Dir$(Drive & ":\*.*", vbVolume), _
         VolumeLabel, vbTextCompare) = 0)
     ' 取回原來磁碟機代號
     ChDrive saveDir
     Exit Function
 Error_Handler:
     ' 如果錯誤為Device Unavailable或Disk Not Ready,則確定是磁碟機
     ' 讓使用者有第二次機會更換磁片
     If (Err = 68 Or Err = 71) And InStr(1, "AB", Drive, _
         vbTextCompare) Then
         answer = MsgBox("Please enter a diskette in drive " & Drive, _
             vbExclamation + vbRetryCancel)
         ' 重試ChDir指令,或離開並回傳False
         If answer = vbRetry Then Resume
     Else
         ' 其它條件下,回傳錯誤給呼叫程式
         Err.Raise Err.Number, Err.Source, Err.Description
     End If
 End Function

至少有5種以上的方法你可以跳離錯誤程序:

On Error Resume Next指令
 

當你不需要回報或追蹤錯誤,On Error Resume Next指令最適合。某些狀況下,你可以安全地略過錯誤程式碼,如:

' 在Form1中隱藏所有控制項
 Dim ctrl As Control
 ' 並非所有控制項都支援Visible屬性(Timer沒有Visible屬性)
 On Error Resume Next
 For Each ctrl In Form1.Controls
     Ctrl.Visible = False
 Next

如果你想測試全部的錯誤條件,必須在錯誤發生時立刻檢驗,不然你可以在一整組的敘述句之後利用Err函數來處理。事實上,如果程序中每一行都發生錯誤,Visual Basic不會重設Err的值直到程式設計者在程式外使用Err.Clear方法清除。

如果錯誤發生時有一個作用的On Error Resume Next指令,程序會繼續從下一行繼續執行。這個特性讓你可以測試控制項及物件的屬性。

' 在Form1中隱藏所有控制項然後恢復
 Dim ctrl As Control, visibleControls As New Collection
 On Error Resume Next
 For Each ctrl In Form1.Controls
     If ctrl.Visible = False Then
         ' 這個控制項不支援Visible屬性
         ' 或控制項已經隱藏:這兩個情況下都不作任何處理
     Else
         ' 記錄這是一個可見控制項並將其隱藏
         visibleControls.Add ctrl
         ctrl.Visible = False
     End If
 Next
 ' 處理其它需要程序(省略)
 ' 然後恢復控制項原來的Visible屬性
 For Each ctrl In visibleControls
     ctrl.Visible = True
 Next

使用On Error Resume Next指令這個非正統的方式是Visual Basic程式設計師十分有力的工具,但是它好像讓你程式碼的邏輯變得有點難懂。我建議你只在其它方式都不行時才用這個辦法,最重要的是,為你的程式碼作詳盡的註解,這樣你的程式是如何執行和為什麼這樣作就都很清楚了。

當程序中包含On Error Resume Next指令時,呼叫程式碼可以看到程序中上個錯誤,比較起來,包含On Error Goto <label> 指令的程序在控制項回傳時會將錯誤碼清除。

非處理錯誤
 

到目前為止,我們已經知道程序發生錯誤時有On Error Resume Next或On Error Goto <line> 這些指令所保護,只要這些錯誤處理指令仍然作用(沒有被取消或被後來的On Error Goto 0指令清除),程序就處於錯誤處理狀態。然而,不是所有的程序都撰寫得很好,所以你必須自己仔細考慮發生的原因(即非預期錯誤)。


說明

任何錯誤處理器處理的錯誤程式碼都適用上述規則,說明了為何要使用Err.Raise方法,並且確定錯誤會被傳回呼叫程序。


下面例子概述了筆者到目前為止的說明,你只要在表單中加入一個叫作Command1的按鈕控制項,然後複製上這些程式碼即可:

Private Sub Command1_Click()
     ' 在下一行作註解看此事件如何觸發
     ' 程序不排除非預期性錯誤的發生
     On Error GoTo Error_Handler
     Print EvalExpression(1)
     Print EvalExpression(0)
     Print EvalExpression(-1)
     Exit Sub
 Error_Handler:
     Print "Result unavailable"
     Resume Next
 End Sub
 Function EvalExpression(n As Double) As Double
     On Error GoTo Error_Handler
     EvalExpression = 1 + SquareRootReciprocal(n)
     Exit Function
 Error_Handler:
     If Err = 11 Then
         '除數為0時回傳-1(沒有回復執行的必要)
         EvalExpression = -1
     Else
         ' 錯誤發生時通報呼叫程序
         Err.Raise Err.Number, Err.Source, Err.Description
     End If
 End Function
 Function SquareRootReciprocal(n As Double) As Double
     ' 這裡可能導致除數為0的錯誤 (Err = 11) 或
     ' 無效的程序呼叫 (Err = 5)
     SquareRootReciprocal = 1 / Sqr(n)
 End Function

執行這個程式並按鈕,你應該看到下列的輸出:

2
 -1
 Result Unavailable

然後註解出Command1_Click程序中的On Error命令來看看發生了什麼及事件何時因為錯誤而結束。


注意

非所有執行時期(run-time)錯誤都可以追蹤,最值得注意的是error28-"Out of stack space." 當這個錯誤發生時,應用程式必然發生嚴重錯誤並結束。但是所有32-bit Visual Basic應用程式都有1 MB的可用堆疊空間,空間用完導致錯誤的情形應該不太可能發生,所以可能是你大量地作了某些重複動作:換言之,你的程序可能呼叫了自己本身並且進入無盡迴圈中。這個典型的程式錯誤通常應該在程式編譯前被修正,所以我並沒有考慮要在執行時期去追蹤這個嚴重的錯誤。


Err物件
 

Visual Basic會自動去關聯每一個錯誤的相關資訊,包括你的自定錯誤。這個功能藉由Err物件提供,Err物件有6個屬性及2個方法,這些屬性中最重要的是Number,它是數值化的錯誤代碼。 Number也是Err物件的預設屬性,所以你可以在你的程式碼中使用Err或Err.Number,它允許向上相容以維護較早版本的Visual Basic,甚至是QuickBasic。

Source屬性會自動填入錯誤發生位置的字串。如果錯誤發生在標準或表單模組,Visual Basic設定其屬性為project的名稱(例如:Project1);如果錯誤發生於類別模組,則其屬性會是這個類別的全名(例如:Project1.Class1)。你可以測試這個屬性來瞭解錯誤發生的位置是應用程式的內部還是外部,你還可以在錯誤通報前修改它的值。

Description屬性會自動填入說明錯誤發生的字串(例如:"Division by Zero")。通常,這個字串對較小代碼的錯誤說明得比較多,你也可以在錯誤通報前修改它。HelpFile與HelpContext屬性會填寫上說明文件的資訊,像是在哪一頁可以找到更多說明及處理方法等。每一個Visual Basic內定的錯誤在說明檔裡都有相關的說明頁,如果你幫其它開發者撰寫程式館,你應該也設計好一個自訂錯誤代碼,並且在關聯好它的說明,這在商業應用程式中應該很少會用得到。最後,LastDllError為唯讀屬性,它會被Visual Basic於API程序處理錯誤時設立,在其他狀況下沒有作用。

Raise方法產生錯誤並且選擇性地指派值給以上所有屬性,它的語法如下:

Err.Raise Number, [Source], [Description], [HelpFile], [HelpContext])

所有參數除了第一個都可選擇性地省略。利用參數命名來取得更多可讀程式碼:

Err.Raise Number:=1001, Description:="Customer Not Found"

Clear方法會重設所有屬性。

Visual Basic的Err物件與不同程序間通報錯誤碼及資訊的COM機制相容,我們會在 第16章 探討這個特性的重要處。

Visual Basic IDE的錯誤處理器
 

到目前為止,筆者說明了編譯過應用程式錯誤發生的情形,然而,當程式碼在IDE內執行時,Visual Basic的處理動作就有點不同了,它會嘗試去簡化你的除錯工作。更明白地說,IDE可以經由工具的Options對話框裡的General標籤頁設定其動作。如圖4-3。以下列出它的一些可能性:

圖4-3的Option對話框為VB環境的預設選項。如果你想要在不改變一般選項的情況下異動錯誤處理模式,在編碼視窗點滑鼠右鍵,然後選擇其中一個Toggle副選單,如圖4-4。這個方式設定較快,而且這樣讓你可以設定多重IDE選項,其中每一個選項都有不同的錯誤處理模式。


 

圖4-3 Options選項對話框裡的General一般標籤頁


 

圖4-4 code Editor內的Toggle快顯功能表。

你終於讀完了本章討論Visual Basic的資料型態,也瞭解一些程式設計指南沒有詳細說明的細節。現在可以準備檢閱VBA提供用來處理這些資料型態的函數了。