跳轉到內容

Visual Basic/編碼標準

來自華夏公益教科書,開放的書籍,為開放的世界

有關版權、許可和本文件作者的詳細資訊,請參見Credits and Permissions

正在進行中。文件需要進行大量重新格式化以符合華夏公益教科書的樣式。此外,頁面太長,不方便在瀏覽器文字框中編輯,因此必須將其分成多個部分。使用原始文件作為指南:http://www.gui.com.au/resources/coding_standards_print.htm

概述

[edit | edit source]

本文件是一份工作文件 - 它不是為了滿足我們擁有“一種”編碼標準的要求,而是在於承認,如果我們都同意在編寫程式碼時採用一套共同的約定,那麼從長遠來看,我們可以讓我們的生活變得更加輕鬆。

不可避免的是,本文件中有許多地方我們不得不簡單地選擇兩個或多個同樣有效的替代方案中的一個。我們試圖真正考慮每個替代方案的相對優缺點,但不可避免地,一些個人偏好會發揮作用。

希望本文件實際上是可讀的。一些標準文件枯燥乏味,讀起來就像在讀白頁一樣。但是,不要認為本文件就不那麼重要,或者應該比它那些枯燥的同類文件更隨便地對待。

本文件適用於何時?

[edit | edit source]

我們的意圖是所有程式碼都符合此標準。但是,在某些情況下,應用這些約定是不切實際或不可能的,而在其他情況下,這樣做是錯誤的。

本文件適用於除以下情況外的所有程式碼:對未按照此標準編寫的現有系統所做的程式碼更改。

總的來說,將你的更改儘可能地符合周圍的程式碼風格是一個好主意。你可以選擇在對現有系統進行重大新增時採用此標準,或者當你新增的程式碼你認為將成為一個已經使用此標準的程式碼庫的一部分時。

為需要採用其標準的客戶編寫的程式碼。

程式設計師與有自己編碼標準的客戶合作並不少見。大多數編碼標準至少從匈牙利表示法概念中獲取了一些內容,尤其是從微軟的一篇白皮書中獲取了一些內容,該白皮書記錄了一套建議的命名標準。因此,許多編碼標準在很大程度上相互相容。本文件在某些方面比大多數標準文件更進一步;但是,這些擴充套件很可能不會與大多數其他編碼標準發生衝突。但讓我們明確一點:如果發生衝突,則應採用客戶的編碼標準。始終如此。

命名標準

[edit | edit source]

在構成編碼標準的所有元件中,命名標準是最直觀的,可以說是最重要的。

在你的程式中為各種“東西”命名使用一致的標準,將為你節省大量時間,既包括開發過程本身,也包括以後的任何維護工作。我說“東西”是因為在 VB 程式中需要命名很多不同的東西,而“物件”一詞有特定的含義。

駝峰式大小寫和全大寫

[edit | edit source]

簡而言之,變數名主體、函式名和標籤名使用駝峰式大小寫,而常量使用全大寫

粗略地說,駝峰式大小寫是指所有單詞的第一個字母大寫,沒有單詞分隔符,隨後的字母小寫,而全大寫是指所有單詞都大寫,並用下劃線分隔。

名稱主體大小寫不適用於前綴和字尾

[edit | edit source]

字首和字尾用於

  • 具有模組作用域的名稱的模組名
  • 變數型別
  • 變數是否為函式引數

這些字首和字尾不使用與名稱主體相同的大小寫規則。以下是一些字首或字尾以粗體顯示的示例。

 ' s shows that sSomeString is of type String
 sSomeString
 ' _IN shows that iSomeString_IN is an input argument
 iSomeInteger_IN
 ' MYPRJ_ shows that MYPRJ_bSomeBoolean has module scope MYPRJ
 MYPRJ_bSomeBoolean


物理單位不應區分大小寫

[edit | edit source]

有時,在變數名中顯示變數值以哪種物理單位表示很方便,例如 mV 代表毫伏,或 kg 代表千克。同樣適用於返回某個實數值的函式名。物理單位應放在末尾,前面是下劃線,不區分大小寫。大小寫對於物理單位具有特殊含義,更改它會非常令人困擾。以下是一些下劃線和單位以粗體顯示的示例。

 fltSomeVoltage_mV
 sngSomePower_W_IN
 dblSomePowerOffset_dB
 dblSomeSpeed_mps

首字母縮略詞

[edit | edit source]

首字母縮略詞單詞有特殊處理。

示例
自由文字 駝峰式大小寫 全大寫 註釋

我開心嗎

AmI_Happy

AM_I_HAPPY

單個字母的單詞全部大寫,並在駝峰式大小寫中用下劃線分隔

GSM 手機

TheGSmPhone

THE_GSM_PHONE

首字母縮略詞在駝峰式大小寫中保持全大寫,並在全大寫中將最後一個字母設定為小寫

DC-DC 轉換器

A_DC_DCConverter

A_Dc_Dc_CONVERTER

當有兩個連續的首字母縮略詞,而第一個首字母縮略詞只有兩個字母時,在駝峰式大小寫中,第一個首字母縮略詞後面跟一個下劃線

GSM LTE 手機

A_GSmLTEPhone

A_GSm_LTe_PHONE

當有兩個連續的首字母縮略詞,而第一個首字母縮略詞有三個或更多字母時,在駝峰式大小寫中,第一個首字母縮略詞的最後一個字母小寫

變數

[edit | edit source]

程式碼中經常使用變數名;大多數語句都包含至少一個變數的名稱。透過為變數使用一致的命名系統,我們可以最大限度地減少花在查詢變數名稱的確切拼寫上的時間。

此外,透過在變數名稱中編碼有關變數本身的資訊,我們可以更容易地破譯使用該變數的任何語句的含義,並捕獲許多難以發現的錯誤。

對變數的屬性進行編碼以將其名稱寫入其名稱中,其屬性對編碼很有用,包括其作用域及其資料型別。

作用域

[edit | edit source]

在 Visual Basic 中,可以在三個作用域中定義變數。如果在過程內部定義,則該變數對該過程是區域性的。如果在窗體或模組的通用宣告區域中定義,則該變數可以從該窗體或模組中的所有過程引用,並被稱為具有模組作用域。最後,如果它使用 Global 關鍵字定義,則它(顯然)對應用程式是全域性的。

過程引數存在一個比較特殊的案例。名稱本身的作用域是區域性的(對過程本身)。但是,在某些情況下,對該引數應用的任何更改都可能影響完全不同作用域的變數。這可能正是您想要發生的事情——它絕不是錯誤——但它也可能是導致微妙且非常令人沮喪的錯誤的原因。

資料型別

[edit | edit source]

VB 支援大量資料型別。它也是一種非常弱型別的語言;您可以在 VB 中將幾乎任何東西扔到任何東西上,它通常會粘住。在 VB4 中,它變得更糟了。由於 VB 在幕後為您執行了意外的轉換,因此隱蔽的錯誤可能會潛入您的程式碼中。

透過在變數名稱中編碼變數的型別,您可以直觀地檢查對該變數的任何賦值是否合理。這將幫助您非常快地發現錯誤。

將資料型別編碼到變數名稱中還有另一個好處,這個好處不常被認為是該技術的優勢:資料名稱的重複使用。如果您需要將開始日期儲存在字串和雙精度數中,那麼您可以為這兩個變數使用相同的根名稱。開始日期始終是 StartDate;它只需要一個不同的標籤來區分它儲存的不同格式。

Option Explicit

[edit | edit source]

首先要做的。始終使用 Option Explicit。原因是顯而易見的,我就不再贅述了。如果您不同意,請與我交談,我們會對此進行友好的討論——呃——友好地討論。

變數名稱

[edit | edit source]

變數的命名方式如下

 scope + type + VariableName

作用域被編碼為單個字元。此字元的可能值為


g 這表示變數的作用域是全域性的
m 這表示變數是在模組(或窗體)級別定義的

沒有作用域修飾符表示變數的作用域是區域性的。

我將使用類似這樣側邊欄的文字嘗試解釋我為什麼做出某些選擇。我希望這對您有所幫助。
一些編碼標準要求使用另一個字元(通常是“l”)來指示區域性變數。我真的不喜歡這樣。我認為這根本不會提高程式碼的可維護性,我認為它會使程式碼更難閱讀。此外,它很難看。

變數的型別被編碼為一個或多個字元。更常見的型別被編碼為單個字元,而不太常見的型別使用三個或四個字元進行編碼。

我對此仔細考慮了很久,夥計們。當然可以為所有內建資料型別設計一個字元程式碼。但是,即使是自己想出來的,也很難記住它們。總的來說,我認為這是一個更好的方法。

型別標籤的可能值為

i integer 16 位有符號整數
l long 32 位有符號整數
s string VB 字串
n numeric 未指定大小的整數(16 位或 32 位)
c currency 64 位整數乘以 10-4
v variant VB 變體
b boolean 在 VB3 中:用作布林值的整數,在 VB4 中:本機布林資料型別
dbl double 雙精度浮點數
sng single 單精度浮點數
flt float 沒有特定精度的浮點數
byte byte 8 位二進位制值(僅限 VB4)
obj object 通用物件變數(後期繫結)
ctl control 通用控制元件變數
我們不使用 VB 的型別字尾字元。考慮到我們的字首,這些是多餘的,無論如何沒有人能記住它們中的幾個。而且我不喜歡它們。

重要的是要注意,為變數的底層實現型別定義字首雖然有一定用處,但通常不是最佳選擇。更實用的是根據資料的底層方面定義字首。例如,考慮日期。您可以將日期儲存在雙精度數或變體中。您通常不關心它是哪一個,因為只有日期可以邏輯地分配給它。

考慮這段程式碼

dblDueDate = Date() + 14

我們知道 dblDueDate 儲存在雙精度變數中,但我們依賴變數本身的名稱來識別它是一個日期。現在我們突然需要處理空日期(例如,因為我們將處理外部資料)。我們需要使用變體來儲存這些日期,以便我們能夠檢查它們是否為空。我們需要更改變數名稱以使用新的字首,並在所有使用它的位置找到它,並確保它也被更改

vDueDate = Date() + 14

但實際上,DueDate 首先是一個日期。因此,應使用日期字首來識別它

dteDue = Date() + 14

此程式碼不受日期底層實現變化的影響。在某些情況下,您可能需要知道它是雙精度數還是變體,在這種情況下,適當的標籤也可以在日期標籤之後使用

  dtevDue = Date() + 14
  dtedblDue = Date() + 14

相同的論點適用於許多其他情況。迴圈索引可以是任何數字型別。(在 VB4 中,它甚至可以是字串!)您經常會看到這樣的程式碼

  Dim iCounter As Integer
  For iCounter = 1 to 10000
    DoSomething
  Next iCounter

現在,如果我們需要處理 100,000 個專案的迴圈呢?我們需要將變數的型別更改為長整數,然後更改其名稱的所有出現位置。

如果我們改為像這樣編寫例程

  Dim nCounter As Integer
  For nCounter = 1 to 10000
    DoSomething
  Next nCounter

我們可以透過僅更改 Dim 語句來更新例程。

Windows 控制代碼是一個更好的例子。控制代碼在 Win16 中是 16 位項,在 Win32 中是 32 位項。將控制代碼標記為控制代碼比標記為整數或長整數更有意義。移植此類程式碼將變得容易得多——您只需更改變數的定義,而其餘程式碼保持不變。

以下是常見資料型別及其標籤的列表。它並不詳盡,您很可能在任何大型專案中都會自己建立幾個。

h handle 16 位或 32 位控制代碼 hWnd
dte date 儲存在雙精度數或變體中 dteDue

變數主體由一個或多個完整的單片語成,每個單詞以大寫字母開頭,例如 ThingToProcess。在形成主體名稱時,需要牢記一些規則。

使用多個單詞——但要謹慎使用。可能很難記住應付金額是 AmountDue 還是 DueAmount。從最基本和通用的東西開始,然後根據需要新增修飾符。金額是一個更基本的東西(什麼是應付的?),因此您應該選擇 AmountDue。類似地

正確 不正確
DateDue DueDate
NameFirst FirstName
ColorRed RedColour
VolumeHigh HighVolume
StatusEnabled EnabledStatus

您通常會發現名詞比形容詞更通用。以這種方式命名變數意味著相關變數往往會一起排序,從而使交叉引用列表更有用。

在處理一組事物(如陣列或表)時,通常會使用一些限定符。一致地使用標準修飾符可以顯著幫助程式碼維護。以下是一些常見修飾符及其應用於事物集時的含義

Count count 集合中專案的數量 SelectedCount
Min minimum 集合中的最小值 BalanceMin
Max maximum 集合中的最大值 RateHigh
First first 集合的第一個元素 CustomerFirst
Last last 集合的最後一個元素 InvoiceLast
Cur current 集合的當前元素 ReportCur
Next next 集合的下一個元素 AuthorNext
Prev previous 集合的上一個元素 DatePrev

一開始,將這些修飾符放在主體名稱之後可能需要一些時間來適應;但是,採用一致方法的好處是真實存在的。

使用者定義型別 (UDT)

[edit | edit source]

UDT 是 VB 支援的一種非常有用(並且經常被忽視)的功能。深入研究它們 - 這不是描述它們如何工作或何時使用的正確文件。我們將重點關注命名規則。

首先,請記住,要建立 UDT 的例項,您必須先定義 UDT,然後使用 Dim 來宣告其例項。這意味著您需要兩個名稱。為了區分型別與該型別的例項,我們使用單個字母字首作為名稱,如下所示

  Type TEmployee
    nID      As Long
    sSurname As String
    cSalary  As Currency
  End Type
  
  Dim mEmployee As TEmployee

我們不能在這裡使用 C 約定,因為它們依賴於 C 區分大小寫的事實。我們需要制定自己的約定。使用大寫字母“T”可能與迄今為止提出的其他約定相矛盾,但從視覺上區分 UDT 定義的名稱和變數的名稱很重要。請記住,UDT 定義不是變數,不佔用任何空間。

除此之外,這就是我們在 Delphi 中的做法。

陣列

[edit | edit source]

無需區分陣列的名稱和標量變數的名稱,因為您始終能夠在看到陣列時識別它,要麼是因為它在括號中包含下標,要麼是因為它被包裝在一個函式中,例如 UBound,而該函式僅對陣列有意義。

陣列名稱應該是複數。這在 VB4 過渡到集合時將特別有用。

您應該始終使用上限和下限對陣列進行維度。而不是

Dim mCustomers(10) as TCustomer

嘗試

Dim mCustomers(0 To 10) as TCustomer

很多時候,建立基於 1 的陣列更有意義。作為一般原則,嘗試建立允許在程式碼中進行簡潔高效的處理的下標範圍。

過程

[edit | edit source]

過程按照以下約定命名

 verb.noun
 verb.noun.adjective

以下是一些示例

良好

 FindCustomer
 FindCustomerNext
 UpdateCustomer
 UpdateCustomerCur

不良

 CustomerLookup should be LookupCustomer
 GetNextCustomer should be GetCustomerNext

VB 中的過程作用域規則相當不一致。過程在模組中是全域性的,除非它們被宣告為 Private;在 VB3 中,它們始終在窗體中是區域性的,或者在 VB4 中預設設定為 Private,但如果明確宣告,則可以是 Public。術語“一團糟”有意義嗎?

由於事件過程無法重新命名並且沒有作用域字首,因此 VB 中的使用者過程也不應包含作用域字首。公共模組應將所有無法從其他模組呼叫的過程保持為 Private。

函式過程資料型別

[edit | edit source]

可以說函式過程具有資料型別,該資料型別是其返回值的型別。這是函式的一個重要屬性,因為它會影響它如何以及在何處可以被正確使用。

因此,函式名稱應該像變數一樣以資料型別標籤為字首。

引數

[edit | edit source]

在過程主體內部,引數名稱具有非常特殊的狀態。名稱本身對過程是區域性的,但名稱所關聯的記憶體可能不是。這意味著更改引數的值可能會對過程本身完全不同的作用域產生影響,這可能是導致後來難以跟蹤的錯誤的原因。最好的解決方案是在一開始就阻止這種情況發生。

Ada 擁有一種非常巧妙的語言機制,透過該機制,所有過程的引數都被標記為 In、Out 或 InOut 中的一種型別。然後編譯器會對過程主體強制執行這些限制;不允許從 Out 引數分配,也不允許對 In 引數分配。不幸的是,沒有主流語言支援此功能,因此我們需要進行一些變通處理。

始終確保您完全清楚每個引數的使用方式。它是用於向過程或呼叫方傳遞值,還是兩者兼而有之?如果可能,請使用 ByVal 宣告輸入引數,以便編譯器強制執行此屬性。不幸的是,有些資料型別不能透過 ByVal 傳遞,尤其是陣列和 UDT。

每個引數名稱都是根據形成變數名稱的規則形成的。當然,引數始終處於過程級別(區域性)作用域,因此不會有作用域字元。在每個引數名稱的末尾,新增一個下劃線,後跟 IN、OUT 或 INOUT 中的其中一個詞,具體取決於情況。使用大寫字母來真正突出顯示程式碼中的這些內容。以下是一個示例

  Sub GetCustomerSurname(ByVal nCustomerCode_IN as Long, _
                         sCustomerSurname_OUT As String)

如果您看到對以 _IN 結尾的變數的賦值(即,如果它位於賦值語句中“=”的左側),那麼您可能遇到了錯誤。同樣,如果您看到對以 _OUT 結尾的變數值的引用,也是如此。這兩個語句都非常可疑

  nCustomerCode_IN = nSomeVariable
  nSomeVariable = nCustomerCode_OUT

函式返回值

[edit | edit source]

在 VB 中,您可以透過將值分配給與函式同名的偽變數來指定函式的返回值。這對於許多語言來說是一個相當常見的構造,並且通常工作正常。

但是,此方案存在一個限制。也就是說,它確實很難將程式碼從一個函式複製貼上到另一個函式,因為您必須更改對原始函式名稱的引用。

始終在每個函式過程內部宣告一個名為 Result 的區域性變數。確保在函式的最開始處為該變數分配一個預設值。在退出點,在函式退出之前立即將 Result 變數分配給函式名稱。這是一個函式過程的骨架(為了簡潔起見,減去了註釋塊)

  Function DoSomething() As Integer
  
    Dim Result As Integer
    Result = 42 ' Default value of function
  
    On Error Goto DoSomething_Error
    ' body of function
  DoSomething_Exit:   
    DoSomething = Result
    Exit Function
  DoSomething_Error:   
    ' handle the error here  
    Resume DoSomething_Exit
  
  End Function

在函式主體中,您應該始終將所需的返回值分配給 Result。您也可以隨意檢查 Result 的當前值。您還可以讀取函式返回值的值。

如果您從未有過使用它的能力,這可能聽起來不是什麼大不了的事,但一旦嘗試過,您將無法再回到沒有它的工作狀態。

不要忘記,函式也有資料型別,因此它們的名稱應該像變數一樣加字首。

常量

[edit | edit source]

常量的格式規則對我們所有人來說都將非常困難。這是因為微軟在常量命名約定方面來了個大轉變。微軟使用兩種常量格式,不幸的是,我們需要同時使用這兩種格式。雖然有人可能會遍歷 MS 定義的常量檔案並將它們轉換為新格式,但這意味著每篇文章、書籍和已釋出的程式碼片段都不會與我們的約定相匹配。

投票未能解決此問題 - 它在兩個備選方案之間幾乎平分秋色 - 所以,我不得不做出一個執行決定,我將嘗試解釋一下。決定是使用舊式的常量格式。

常量以 ALL_UPPER_CASE 編碼,單詞之間用下劃線隔開。如果可能,請使用 Constant.Txt 檔案中定義的常量名稱 - 不是因為它們格式特別好或一致,而是因為它們是您在文件、書籍和雜誌文章中會看到的。

常量的格式可能會隨著世界向 VB4 及更高版本發展而改變,在這些版本中,常量透過型別庫由 OLE 物件公開,而標準格式是使用以一些唯一的、小寫標籤(如 vbYesNoCancel)為字首的首字母大寫字母。

在編寫要從多個地方呼叫的模組(尤其是如果它將在多個程式中使用)時,請定義可以被客戶端程式碼使用的全域性常量,而不是使用魔數或字元。當然,在內部也使用它們。在定義此類常量時,請為每個常量新增一個唯一的名稱字首。例如,如果我正在開發一個 Widget 控制元件模組,我可能會定義如下常量

  Global Const WDGT_STATUS_OK = 0
  Global Const WDGT_STATUS_BUSY = 1
  Global Const WDGT_STATUS_FAIL = 2
  Global Const WDGT_STATUS_OFF = 3
  Global Const WDGT_ACTION_START = 1
  Global Const WDGT_ACTION_STOP = 2
  Global Const WDGT_ACTION_RAISE = 3
  Global Const WDGT_ACTION_LOWER = 4

@TODO: 全域性與 Public @TODO: 提到列舉。

您明白我的意思。這樣做的邏輯是避免與為其他模組定義的常量發生命名衝突。

常量必須透過兩種方式之一指示作用域和資料型別 - 它們可以使用用於變數的小寫作用域和型別字首,或者可以使用大寫特定於組的標籤。上面的 Widget 常量演示了後一種型別:作用域是 WDGT,資料型別是 STATUS 或 ACTION。用於從公共模組的客戶端隱藏實現細節的常量通常使用後一種型別,而用於程式主部分的便利性和可維護性的常量通常使用正常的類似變數的字首。

我覺得需要進一步解釋這些決定,我認為這意味著我對這個結果並不完全滿意。雖然可以很好地認為我們可以繼續定義我們自己的標準,但事實是,我們是更廣泛的 VB 程式設計社群的一部分,需要與其他人(尤其是微軟)編寫的程式碼進行互動。因此,我需要牢記其他人已經做了什麼以及他們在未來可能會做什麼。
決定採用舊式的註釋(即 ALL_UPPER_CASE)是基於以下事實:我們有大量的程式碼(尤其是包含常量定義的模組)需要併入我們的程式。微軟的新格式(vbThisIsNew 格式)僅在型別庫中實施。VB4 文件建議我們採用小寫字首“con”表示應用程式特定的常量(如 conThisIsMyConstant),但在與全球各地的企業開發人員進行的幾次線上討論中,似乎並沒有採用這種方法,或者至少在可預見的未來沒有采用。
這就是我決定使用舊式註釋的主要原因。我們熟悉它們,其他人都在使用它們,並且它們清楚地區分了程式中的程式碼(無論是我們編寫的還是從供應商提供的 BAS 檔案中包含的程式碼)與由型別庫釋出的常量以及 VB4+ 中引用的常量。
另一個難題是常量作用域和型別標籤的問題。大多數人都希望它們,儘管它們通常沒有在供應商提供的常量定義中定義。我自己對型別標籤不太確定,因為我認為有時隱藏型別很有用,可以使客戶端程式碼不那麼依賴於一些公共程式碼的實現。
最終,選擇了折衷方案。對於型別很重要的常量(因為它們用於將值分配給程式自己的資料項),使用正常的變數樣式標籤來表示範圍和型別。對於“介面”常量,例如供應商提供的與控制元件互動的常量,範圍可以被認為是控制元件,而資料型別則是該控制元件特定屬性或方法支援的特殊型別之一。換句話說,MB_ICONSTOP 的範圍是 MB(訊息框),資料型別是 ICON。當然,我認為它應該叫 MB_ICON_STOP,而 MB_YESNOCANCEL 應該叫 MB_BUTTONS_YESNOCANCEL,但是你永遠不會指責微軟不一致。我希望這能解釋我為什麼決定這樣做。
我懷疑這個問題會隨著我們都在應用此標準方面獲得一些經驗而產生進一步的討論。

控制元件

[edit | edit source]

窗體上的所有控制元件都應該從預設的 Textn 名稱重新命名,即使是最簡單的窗體也是如此。

唯一的例外是任何僅用作窗體表面上的靜態提示的標籤。如果標籤在程式碼中被引用,即使只引用一次,它也必須與其他控制元件一起被賦予有意義的名稱。否則,它們的名稱並不重要,可以保留為預設的“Labeln”名稱。當然,如果你真的熱衷於此,你可以給他們賦予有意義的名稱,但我真的認為我們都很忙,可以安全地跳過這一步。如果你不喜歡將這些名稱設定為預設值(我承認我屬於這個群體),你可能會發現以下技術很有用。建立一個包含窗體上所有惰性標籤的控制元件陣列,並將該陣列命名為 lblPrompt( )。這樣做最簡單的方法是按照你想要的方式建立第一個標籤(使用合適的字型、對齊方式等),然後複製並貼上它,直到創建出所有標籤。使用控制元件陣列還有一個額外的好處,因為它在窗體的名稱表中只佔用一個名稱。

在為窗體編寫任何程式碼之前,花點時間重新命名所有控制元件。這是因為程式碼是透過其名稱附加到控制元件上的。如果你在事件過程中編寫了一些程式碼,然後更改了控制元件的名稱,你就會建立一個孤兒事件過程。

控制元件有自己的一組字首。它們用於標識控制元件的型別,以便可以直觀地檢查程式碼的正確性。它們還有助於輕鬆地瞭解控制元件的名稱,而無需不斷地查詢。(參見下面的“資料項從搖籃到墳墓的命名”。)

指定特定控制元件變體 - 不推薦

[edit | edit source]

一般來說,使用特定識別符號來表示主題的變體不是一個好主意。例如,無論是使用三維按鈕還是標準按鈕,對於你的程式碼來說通常是不可見的——你可能擁有更多屬性來在設計時玩弄,以在視覺上增強控制元件,但你的程式碼通常會捕獲 Click() 事件,也許會操作 Enabled 和 Caption 屬性,這些屬性對於所有類似按鈕的控制元件都是通用的。

使用通用字首意味著你的程式碼對應用程式中使用的特定控制元件變體依賴性較小,因此簡化了程式碼重用。只有當你的程式碼完全依賴於該特定控制元件的某個獨特屬性時,才區分基本控制元件的變體。否則,儘可能使用通用字首。標準控制元件字首表

下表列出了你會遇到的常見控制元件型別及其字首

字首 控制元件
cbo 組合框
chk 複選框
cmd 命令按鈕
dat 資料控制元件
dir 目錄列表框
dlg 通用對話方塊控制元件
drv 驅動器列表框
ela 彈性
fil 檔案列表框
fra 框架
frm 窗體
gau 儀表
gra 圖表
img 影像
lbl 標籤
lin
lst 列表框
mci MCI 控制元件
mnu 選單控制元件 †
mpm MAPI 訊息
mps MAPI 會話
ole OLE 控制元件
opt 選項按鈕
out 大綱控制元件
pic 圖片
pnl 面板
rpt 報表
sbr 捲軸(無需區分方向)
shp 形狀
spn 旋轉
ssh 電子表格控制元件
tgd Truegrid
tmr 計時器 ‡
txt 文字框

† 選單控制元件遵循下面定義的額外規則。

‡ 專案中通常只有一個計時器控制元件,它用於多個事物。這使得很難想出一個有意義的名稱。在這種情況下,可以將控制元件簡單地命名為“Timer”。

[edit | edit source]

選單控制元件應使用“mnu”標籤加上選單樹的完整路徑進行命名。這還有一個額外的好處,它鼓勵使用者介面設計中通常被認為是好事兒的淺層選單層次結構。

以下是一些選單控制元件名稱的示例

 mnuFileNew
 mnuEditCopy
 mnuInsertIndexAndTables
 mnuTableCellHeightAndWidth

資料項從搖籃到墳墓的命名

[edit | edit source]

儘管前面所有的規則都很重要,但如果你沒有進行這一最後一步,即我所說的從搖籃到墳墓的命名,那麼你在思考物件命名上花費的所有額外時間所獲得的回報將會很小。這個概念很簡單,但要約束自己始終做到這一點卻非常困難。

從本質上講,這個概念僅僅承認任何給定的資料項只有一個名稱。如果某樣東西叫 CustomerCode,那麼它在任何地方都叫 CustomerCode。不是 CustCode。不是 CustomerID。不是 CustID。不是 CustomerCde。除了 CustomerCode 之外,沒有其他名稱是可以接受的。

現在,假設客戶程式碼是一個數字項。如果我需要一個客戶程式碼,我會使用名稱 nCustomerCode。如果我想在一個文字框中顯示它,那麼該文字框必須被稱為 txtCustomerCode。如果我想讓一個組合框顯示客戶程式碼,那麼該控制元件應該被稱為 cboCustomerCode。如果你需要將一個客戶程式碼儲存在全域性變數中(我不知道你為什麼要這樣做——我只是在說明問題),那麼它應該被稱為 gnCustomerCode。如果你想將它轉換為字串(比如稍後打印出來),你可能會使用類似這樣的語句

sCustomerCode = Format$(gnCustomerCode)

我認為你明白了。這真的很簡單。要做到每次都這樣做也異常困難,只有每次都這樣做才能獲得真正的回報。這樣做起來真的非常誘人,就像這樣編寫上面的程式碼

sCustCode = Format$(gnCustomerCode)

資料庫中的欄位

[edit | edit source]

作為一個通用的規則,應該使用資料型別標籤字首來命名資料庫中的欄位。

這可能並不總是實用的,甚至可能不可行。如果資料庫已經存在(要麼是因為新的程式引用了現有的資料庫,要麼是因為資料庫結構是在資料庫設計階段建立的),那麼在每列或欄位上應用這些標籤是不切實際的。即使對於現有資料庫中的新表,也不要偏離該資料庫中(希望)已經使用的約定。

一些資料庫引擎不支援資料項名稱中的混合大小寫。在這種情況下,像 SCUSTOMERCODE 這樣的名稱在視覺上很難掃描,省略標籤可能是一個更好的主意。此外,某些資料庫格式只允許非常短的名稱(例如 xBase 的 10 個字元限制),因此你可能無法將標籤都放進去。

但是,一般來說,你應該在資料庫欄位/列名之前加上資料型別標籤。

物件

[edit | edit source]

在 VB 中經常使用幾種物件型別。最常見的物件(及其型別標籤)是

字首 物件
db 資料庫
ws 工作區
rs 記錄集
ds 動態集
ss 快照
tbl
qry 查詢
tdf 表定義
qdf 查詢定義
rpt 報表
idx 索引
fld 欄位
xl Excel 物件
wrd Word 物件

控制元件物件變數

[edit | edit source]

控制元件物件是指向實際控制元件的指標。當你將一個控制元件分配給一個控制元件物件變數時,你實際上是將一個指向實際控制元件的指標分配給它。然後,對該物件變數的任何引用都將引用它指向的控制元件。

通用控制元件變數定義為

Dim ctlText As Control

這允許透過 Set 語句將指向任何控制元件的指標分配給它

Set ctlText = txtCustomerCode

使用泛型控制元件變數的優點是可以指向任何型別的控制元件。可以使用 TypeOf 語句來測試泛型控制元件變數當前指向的控制元件型別。

If TypeOf ctlText Is Textbox Then

要注意的是,這看起來像一個普通的 If 語句。不能將 TypeOf 與其他測試結合使用,也不能在 Select 語句中使用它。

變數可以指向任何控制元件型別,這也是它的弱點。由於編譯器不知道控制元件變數在任何時候都將指向什麼,因此它無法檢查你的操作是否合理。它必須使用執行時程式碼來檢查對該控制元件變數的每個操作,因此這些操作效率較低,這隻會加劇 VB 在效能方面的名聲。更重要的是,你最終可能會讓變數指向意外的控制元件型別,導致程式崩潰(如果你幸運的話)或執行不正確。

特定控制元件變數也是指標。然而,在這種情況下,變數被限制為只能指向特定型別的控制元件。例如

Dim txtCustomerCode as Textbox

這裡的問題是,我使用了相同的三個字母標籤來表示控制元件物件變數,就像我用來命名窗體上的實際文字控制元件一樣。如果我遵循這些指南,那麼我將在窗體上有一個名為相同名稱的控制元件。我該如何將它分配給變數呢?

我可以使用窗體的名稱來限定對實際控制元件的引用,但這充其量是令人困惑的。出於這個原因,物件控制元件變數名稱應該與實際控制元件不同,因此我們透過在前面使用一個字母 "o" 來擴充套件型別程式碼。前面的定義更準確的應該是

Dim otxtCustomerCode as Textbox

這也是我必須認真思考的一個問題。如果 "o" 位於標籤後面,比如 txtoCustomerName,我實際上更喜歡這個名稱。然而,在所有這些字元中看到那個小小的 "o" 太難了。我也考慮過使用 txtobjCustomerName,但我認為這有點太長了。我很樂意看看其他任何替代方案,或針對這些方案的意見。

實際上,這不像你想象的那麼大問題,因為在大多數情況下,這些變數被用作泛型函式呼叫的引數。在這種情況下,我們通常會使用標籤本身作為引數的名稱,因此不會出現這個問題。

控制元件物件變數的一個非常常見的用法是作為過程的引數。以下是一個示例

  Sub SelectAll(txt As TextBox)
    txt.SelStart = 0
    txt.SelLength = Len(txt.Text)
  End Sub

如果我們在文字框的 GotFocus 事件中放置對該過程的呼叫,那麼每當使用者切換到該文字框時,該文字框的所有文字都會被突出顯示(選中)。

請注意,我們如何透過省略名稱主體來表示這是一個對任何文字框控制元件的泛型引用。這也是 C/C++ 程式設計師常用的技巧,因此你將在 Microsoft 文件中看到這些示例。

為了展示泛型物件變數如何非常有用,請考慮我們需要在應用程式中使用一些帶掩碼的編輯控制元件以及標準文字控制元件的情況。例程的先前版本僅適用於標準文字框控制元件。我們可以按如下方式更改它

  Sub SelectAll(ctl As Control)
    ctl.SelStart = 0  
    ctl.SelLength = Len(ctl.Text)
  End Sub

透過將引數定義為泛型控制元件引數,現在允許我們將任何控制元件傳遞給該過程。只要該控制元件具有 SelStart、SelLength 和 Text 屬性,此程式碼就能正常工作。如果它沒有這些屬性,那麼它將失敗並出現執行時錯誤;使用後期繫結意味著編譯器無法保護我們。要將此程式碼投入生產狀態,請新增對要支援的所有已知控制元件型別的特定測試(只處理這些控制元件),或者新增一個簡單的錯誤處理程式,以便在控制元件不支援所需的屬性時正常退出。

  Sub SelectAll(ctl As Control)
    On Error Goto SelectAll_Error
    ctl.SelStart = 0  ctl.SelLength = Len(ctl.Text)
  SelectAll_Exit:  
    Exit Sub
  SelectAll_Error:
    Resume SelectAll_Exit
  End Sub

API 宣告

[edit | edit source]

如果你不知道如何進行 API 宣告,請參考任何一本關於該主題的優秀書籍。關於 API 宣告沒什麼好說的;要麼做對,要麼就無法執行。

但是,每當你嘗試使用本身使用 API 呼叫的公共模組時,就會出現一個問題。不可避免地,你的應用程式也進行過其中一個呼叫,因此你需要刪除(或註釋掉)其中一個宣告。嘗試新增幾個這樣的宣告,你最終會陷入混亂;API 宣告遍佈各處,每個模組的新增或刪除都會引發一場巨大的 API 宣告搜尋。

然而,有一種簡單的技術可以解決這個問題。所有非可共享程式碼都使用幫助檔案中定義的標準 API 宣告。所有公共模組(那些可能在專案之間共享的模組)都禁止使用標準宣告。相反,如果這些公共模組之一需要使用 API,它必須為該 API 建立一個別名宣告,其中名稱以唯一程式碼為字首(與用作其全域性常量的相同字首)。

如果傳說中的 Widgets 模組需要使用 SendMessage,它 *必須* 宣告

  Declare Function WDGT_SendMessage Lib "User" Alias "SendMessage" _
                  (ByVal hWnd As Integer, ByVal wMsg As Integer, _
                   ByVal wParam As Integer, lParam As Any) As Long

這實際上建立了一個 SendMessage 的私有宣告,可以將其包含在任何其他專案中,而不會出現命名衝突。

原始檔

[edit | edit source]

不要以 "frm" 或 "bas" 開標頭檔案名稱;這就是副檔名的用途!只要我們受到 Dos/Win16 中 8 個字元名稱的限制,我們就需要我們能得到的每個字元。

按照以下方式建立檔名

 SSSCCCCV.EXT

其中 SSS 是一個三個字元的系統或子系統名稱,CCCC 是一個四個字元的窗體名稱程式碼,V 是一個(可選的)單個數字,可用於表示同一檔案的不同版本。EXT 是 VB 分配的標準副檔名,可以是 FRM/FRX、BAS 或 CLS。

@TODO: 重寫特定於短名稱的部分,更新到 win32。

以系統或子系統名稱開頭非常重要,因為在 VB 中很容易將檔案儲存在錯誤的目錄中。使用唯一程式碼允許我們使用目錄搜尋(掃描程式)程式查詢檔案,並最大程度地減少覆蓋屬於其他程式的另一個檔案的可能性,特別是對於常見的窗體名稱,如 MAIN 或 SEARCH。

將最後一個字元保留用於版本號,允許我們在進行可能需要撤消的重大更改之前,對特定檔案進行快照。雖然在可能的情況下應該使用 SourceSafe 等工具,但不可避免地會有你必須在沒有這些工具的情況下工作的情況。要對檔案進行快照,你只需將其從專案中刪除,切換到 Dos 框或資源管理器(或檔案管理器或任何其他工具)來建立具有下一個版本號的新檔案副本,然後新增新的檔案。不要忘記在對窗體檔案進行快照時,也要複製 FRX 檔案。編碼標準

本文件的其餘部分討論與編碼實踐相關的問題。我們都知道,沒有一套規則可以始終盲目地應用,並能產生好的程式碼。程式設計不是藝術形式,但也不是工程學。它更像是一種工藝:有一些公認的規範和標準,以及一個正在慢慢被編纂和正式化的知識體系。程式設計師透過從之前的經驗中學習,以及檢視其他人寫的好的程式碼和壞的程式碼來提高自己的水平。尤其是透過維護糟糕的程式碼。

與其建立一套必須嚴格遵守的規則,我嘗試建立一套原則和指南,這些原則和指南將識別你需要考慮的問題,並在可能的情況下指明好的、壞的和醜陋的替代方案。

每個程式設計師都應該負起責任,建立好的程式碼,而不僅僅是符合某些嚴格標準的程式碼。這是一個更加崇高的目標——它既能給程式設計師帶來更大的滿足感,又能對組織更有用。

基本原則就是簡單。

避免混淆。

過程長度

[edit | edit source]

在程式設計學術界,存在一個都市傳說,即長度不超過 "一頁"(無論那是什麼)的短過程更好。實際的研究表明,這根本不正確。有幾項研究表明恰恰相反。要檢視這些研究(以及指向研究本身的指標),請參閱史蒂夫·麥康奈爾撰寫的《程式碼大全》(Microsoft Press,ISBN 1-55615-484-4,這是一本非常值得一讀的書。讀三遍。)

概括地說,硬性經驗資料表明,隨著你從小型(<32 行)例程轉移到大型例程(約 200 行),例程的錯誤率和開發成本都會下降。程式碼的可理解性(以計算機科學學生為測量物件)與程式碼被超模組化成大約 10 行的例程相比,沒有任何例程的程式碼並沒有更好。相反,在將相同程式碼模組化成大約 25 行的例程時,學生的成績提高了 65%。

這意味著寫長程式碼段並沒有罪過。讓流程的要求決定程式碼段的長度。如果你覺得這段程式碼應該有 200 行,那就寫吧。當然要注意不要過度。程式碼段的長度存在一個上限,超過這個上限就幾乎不可能理解程式碼段了。對大型軟體(例如 IBM 的 OS/360 作業系統)的研究表明,最容易出錯的程式碼段是超過 500 行的程式碼段,錯誤率與超過這個數字的長度大致成正比。

當然,一個過程應該只做一件事。如果你在過程名稱中看到 "And" 或 "Or",你可能做錯了什麼。確保每個過程都具有高內聚性和低耦合性,這是良好結構化設計的基本目標。

首先編寫程式碼中的正常路徑,然後編寫異常情況。編寫程式碼時,應使程式碼中的正常路徑清晰可見。確保異常情況不會掩蓋正常的執行路徑。這對可維護性和效率都很重要。

確保你對相等情況進行正確的分支。一個非常常見的錯誤是使用 > 代替 >= 或反之。

將正常情況放在 If 之後,而不是放在 Else 之後。與流行觀點相反,程式設計師並沒有真正地對否定條件感到困難。建立條件,使 Then 子句對應於正常處理。

在 If 之後使用有意義的語句。這與上一條建議有些相關。不要僅僅為了避免使用 Not 而編寫空 Then 子句。哪個更容易理解

  If EOF(nFile) Then
    ' do nothing
  Else
    ProcessRecord
  End IF

  If Not EOF(nFile) Then
    ProcessRecord
  End If

總是至少考慮使用 Else 子句。通用汽車公司對程式碼的一項研究表明,只有 17% 的 If 語句包含 Else 子句。後來的研究表明,應該有 50% 到 80% 的 If 語句包含 Else 子句。誠然,這是 1976 年的 PL/1 程式碼,但這個資訊不祥。你確定你不需要這個 Else 子句嗎?

使用布林函式呼叫簡化複雜條件。與其在 If 語句中測試 12 件事,不如建立一個返回 True 或 False 的函式。如果你給它一個有意義的名稱,它可以使 If 語句非常易讀,並顯著改程序序。

如果 Select Case 語句可以完成,就不要使用 If 語句鏈。Select Case 語句通常比一系列 If 語句更合適。唯一的例外是使用 TypeOf 條件時,它不適用於 Select Case 語句。

SELECT CASE

[編輯 | 編輯原始碼]

將正常情況放在首位。這樣更易讀,也更高效。

按頻率排序情況。情況會按它們在程式碼中出現的順序進行評估,因此,如果某個情況將被選擇 99% 的時間,就將其放在首位。

保持每個情況的操作簡單。每個情況的程式碼不要超過幾行。如果有必要,請建立從每個情況呼叫的過程。

僅將 Case Else 用於合法預設值。切勿僅為了避免編寫特定測試而使用 Case Else。

使用 Case Else 檢測錯誤。除非你確實有一個預設值,否則請捕獲 Case Else 條件,並顯示或記錄錯誤訊息。

在編寫任何結構時,如果程式碼變得更易讀和更易維護,就可以打破這些規則。將正常情況放在首位的規則就是一個很好的例子。雖然它在一般情況下是好的建議,但在某些情況下(如果你原諒這個雙關語)它會影響程式碼的質量。例如,如果你在給分數評分,分數低於 50 分是不及格,50 分到 60 分是 E,以此類推,那麼 "正常" 且更常見的情況應該是 60 到 80 分,然後像這樣交替出現

  Select Case iScore
  
    Case 70 To 79:   sGrade = "C"
  
    Case 80 To 89:   sGrade = "B"
  
    Case 60 To 69:   sGrade = "D"
  
    Case Is < 50:    sGrade = "F"
  
    Case 90 To 100:  sGrade = "A"
  
    Case 50 To 59:   sGrade = "E"
  
    Case Else:     ' they cheated
  
  End Select

但是,自然編碼方式是遵循分數的自然順序

  Select Case iScore
  
    Case Is < 50:   sGrade = "F"
  
    Case Is < 60:   sGrade = "E"
  
    Case Is < 70:   sGrade = "D"
  
    Case Is < 80:   sGrade = "C"
  
    Case Is < 90:   sGrade = "B"
  
    Case Is <= 100: sGrade = "A"
  
    Case Else:     ' they cheated
  
  End Select

這不僅更容易理解,而且還有額外的優勢,即更加健壯 - 如果分數後來從整數更改為允許小數點,那麼第一個版本會將 89.1 視為 A,這可能不是預期的結果。

另一方面,如果此語句被確定為導致程式效能不夠快的瓶頸,那麼按其發生的統計機率排序情況將是相當合適的,在這種情況下,你將在註釋中記錄這樣做的原因。

包含此討論是為了強調我們並不是在盲目地遵守規則 - 我們試圖編寫好的程式碼。必須遵循這些規則,除非它們導致不好的程式碼,在這種情況下,就不應該遵循這些規則。

保持迴圈體在螢幕上可見。如果迴圈體太長而無法顯示,那麼很有可能它太長而無法理解,應該將其作為過程移出迴圈。

將巢狀限制為三級。研究表明,程式設計師理解迴圈的能力在超過三級巢狀後會顯著下降。

參見上面的 DO。

永遠不要省略 Next 語句中的迴圈變數。如果迴圈的結束點不能正確識別,則很難解開迴圈。

儘量不要使用 i、j 和 k 作為迴圈變數。你當然可以想出一個更有意義的名稱。即使是像 iLoopCounter 這樣的通用名稱也比 i 好。

這只是一個建議。我知道當單個字元的迴圈變數名稱在迴圈中用於許多事物時,它們是多麼方便,但請考慮一下你在做什麼,以及那個可憐的程式設計師,他必須弄清楚 i 和 j 實際上代表什麼。

賦值語句 (Let)

[編輯 | 編輯原始碼]

不要為賦值語句編寫可選的 Let 關鍵字。(皮特,這是說你。)

不要使用 Goto 語句,除非它們使程式碼更簡單。普遍的共識是 Goto 語句往往使程式碼難以理解,但在某些情況下,事實恰恰相反。

在 VB 中,你必須使用 Goto 語句作為錯誤處理的組成部分,因此在這方面你沒有選擇。

你也可以使用 Goto 從非常複雜的巢狀控制結構中退出。這裡要小心;如果你真的覺得需要使用 Goto,那麼可能是控制結構太複雜了,你應該將程式碼分解成更小的例程。

這並不是說沒有使用 Goto 的最佳方法的情況。如果你真的覺得有必要,就使用它。只要確保你已經考慮過它,並確信它確實是一件好事,而不是一個駭客手段。

EXIT SUB 和 EXIT FUNCTION

[編輯 | 編輯原始碼]

與使用 Goto 相關的是 Exit Sub(或 Exit Function)語句。基本上有三種方法可以使程式碼的尾部部分不執行:將其作為條件(If)語句的一部分

  Sub DoSomething()
  
    If CanProceed() Then
  
      . . .
  
      . . .
  
    End If
  
  End Sub

使用 Goto 跳過它

  Sub DoSomething()
  
    If Not CanProceed() Then
  
      Goto DoSomething_Exit
  
    End If
  
    . . .
  
    . . .
  
  DoSomething_Exit:
  
  End Sub

使用 Exit Sub/Function 提早退出

  Sub DoSomething()
  
    If Not CanProceed() Then
    
      Exit Sub
  
    End If
  
    . . .
  
    . . .
  
  End Sub

在這些簡單的、骨架式的例子中,似乎最清晰的是第一個方法,它通常是編寫簡單過程的一種很好的方法。當過程的主體(上面用 '...' 表示)巢狀在確定是否應該執行該主體所需的多個控制結構中時,這種結構就會變得笨拙。在最終的控制結構的巢狀層級中,主體程式碼可能會縮排到螢幕的中間位置。如果該主體本身很複雜,程式碼看起來會很亂,更不用說你之後需要解開這些巢狀的控制結構了。

當你發現在你的程式碼中發生了這種情況時,請採用不同的方法:確定你是否應該繼續,如果不能,請提早退出。這兩種技術都有效。雖然我認為 Exit 語句更 "優雅",但我被迫強制使用 Goto ExitLabel 作為標準。選擇它的原因是,有時你可能需要在退出過程之前進行清理。使用 Goto ExitLabel 結構意味著你可以只編寫一次清理程式碼(在標籤之後),而不是多次編寫(在每個 Exit 語句之前)。

如果你需要提早退出過程,請優先使用 Goto ExitLabel 結構,而不是 Exit Sub 或 Exit Function 語句,除非沒有機會在這些語句之前進行任何清理。

Exit 語句非常適合與守門員變數結合使用,以避免意外遞迴。你知道

  Sub txtSomething_Change()
    Dim bBusy As Integer
    If bBusy Then Exit Sub
    bBusy = True
      . . . ' some code that may re-trigger the Change() event
    bBusy = False
  End Sub

EXIT DO/FOR

[edit | edit source]

這些語句過早地退出包含的 Do 或 For 迴圈。在適當的情況下使用它們,但要小心,因為它們可能會使理解程式執行流程變得困難。

另一方面,使用這些語句可以避免不必要的處理。我們始終以正確性和可維護性為目標進行編碼,而不是效率,但沒有必要進行完全不必要的處理。特別是,不要這樣做

  For nIndex = LBound(sItems) to UBound(sItems)
    If sItems(nIndex) = sSearchValue Then
       bFound = True
       nFoundIndex = nIndex
    End If
  Next nIndex

  If bFound Then . . .

這將始終遍歷陣列的所有元素,即使在第一個元素中找到該專案。在 If 塊中放置 Exit For 語句將提高效能,而不會降低程式碼的清晰度。

避免在深度巢狀迴圈中使用這些語句。 (實際上,首先要避免深度巢狀迴圈。)有時確實沒有選擇,所以這不是一個硬性規定,但總的來說,很難確定 Exit For 或 Exit Do 在深度巢狀迴圈中將分支到哪裡。

GOSUB

[edit | edit source]

Gosub 語句在 VB 中很少使用,而且有充分的理由。在大多數情況下,它並沒有比使用標準 Sub 過程更具優勢。實際上,由 Gosub 執行的例程實際上與呼叫程式碼在同一範圍內,這通常是一個危險的情況,應該避免。

但是,這種屬性本身在某些情況下非常有用。如果您正在編寫一個複雜的 Sub 或 Function 過程,您通常會嘗試識別可以作為單獨的例程實現的離散處理單元,該例程可以根據需要被此過程呼叫。然而,在某些特殊情況下,您會發現需要在這些相關過程之間傳遞的資料量變得相當荒謬。在這些情況下,將這些子例程作為主過程中的子例程實現可以顯著簡化邏輯,並提高程式碼的清晰度和可維護性。

我發現這種技術有用的特殊情況是在建立多級報表時。您最終會得到像 ProcessDetail、ProcessGroup、PrintTotals、PrintSubTotals、PrintHeadings 等等這樣的過程,它們都需要訪問和修改一個共同的資料項池,比如當前頁碼和行號、當前的小計和大計、當前和上一個鍵值(以及我現在想不起來的很多其他東西)。

對於這種情況,本標準允許在適當的情況下使用 Gosub 語句。但是,在決定使用這種方法時要小心。將包含的 Sub 或 Function 視為一個單獨的 COBOL 程式;從控制和作用域的角度來看,它本質上就是這樣的。特別是,確保每個子例程的末尾只有一個 Return 語句。還要確保在所有子例程程式碼之前有一個 Exit Sub 或 Exit Function 語句,以避免處理繼續到該程式碼中(這非常令人尷尬)。

過程呼叫

[edit | edit source]

自從 Dos 上的 QuickBASIC 出現以來,一直存在關於是否應該對非函式過程呼叫使用“Call”關鍵字的激烈爭論。最終,這是一個沒有明確“正確”做法的問題。

本標準棄用使用 Call 關鍵字。

我的理由是,程式碼讀起來更自然。如果我們有命名的良好的過程(使用 verb.noun.adjective 格式),那麼生成的 VB 程式碼幾乎是虛擬碼。此外,我認為如果很明顯發生了什麼,就沒有必要編碼關鍵字,就像在賦值語句中省略 Let 關鍵字一樣。

過程引數

[edit | edit source]

使用過程的問題之一是記住引數的順序。您想要嘗試避免需要不斷檢視過程定義以確定需要提供哪些順序的引數以匹配引數。

以下是一些解決此問題的指導原則。

首先,在所有輸出引數之前編碼所有輸入引數。對 InOut 引數使用您的判斷。通常,您不會在單個過程中擁有所有三種類型;如果您這樣做,我可能會編碼所有輸入引數,然後是 InOut,最後是輸出。再次,對此使用您的判斷。

不要編寫需要大量引數的過程。如果您確實需要將大量資訊傳遞到過程或從過程傳遞資訊,那麼建立一個包含所需引數的使用者定義型別並傳遞該型別。這允許呼叫程式透過在 UDT 中“設定”它們來以任何方便的順序指定值。這是一個相當瑣碎的示例,演示了這種技術

  Type TUserMessage
    sText          As String
    sTitle         As String
    nButtons       As Integer
    nIcon          As Integer
    nDefaultButton As Integer
    nResponse      As Integer
  End Type
  
  Sub Message (UserMessage As TUserMessage)
    '  << comment block omitted for brevity >>
    UserMessage.Response = MsgBox(UserMessage.sText,_
                                  UserMessage.nButtons Or _
                                  UserMessage.nIcon Or _
                                  UserMessage.nDefaultButton, _
                                  UserMessage.sTitle)
  End Sub

現在是呼叫程式碼

  Dim MessageParms As TUserMessage
  
  MessageParms.sText = "Severe error in some module."
  MessageParms.sTitle = "Error"
  
  MessageParms.nButtons = MB_YESNOCANCEL
  MessageParms.nIcon = MB_ICONSTOP   
  MessageParms.sDefaultButton = MB_DEFBUTTON1
  
  Message MessageParms
  
  Select Case MassageParms.nResponse   
     Case ID_YES:   
     Case IS_NO:
     Case ID_CANCEL:
  End Select

錯誤處理

[edit | edit source]

理想的情況是在每一段程式碼周圍都有一個錯誤處理程式,這應該是起點。在實踐中,由於各種原因,這並不總是可行的,而且也不總是必要的。但是,規則是,除非您絕對確定某個例程不需要錯誤處理程式,否則您至少應該在整個例程周圍建立一個處理程式,如下所示

請注意,下一節討論了預設錯誤處理程式,它需要此框架的擴充套件形式。有關具有錯誤處理程式的過程的更完整框架,請參閱該部分。它沒有包含在這裡,因為我們正在本節中檢視標準的其他元件。
  Sub DoSomething()
    On Error Goto HandleError   
    . . .
  Done:
    Exit Sub
  HandleError:
    Resume Done
  End Sub

在過程的主體中,您可以建立需要不同錯誤處理的區域。直白地說,VB 在這個領域很糟糕。我喜歡的技術是暫時停用錯誤捕獲,並使用內聯程式碼測試錯誤,如下所示

  DoThingOne
  DoThingTwo
  
  On Error Resume Next
  
  DoSomethingSpecial
    
  If Err Then
    HandleTheLocalError      
  End If
     
  On Error Goto HandleError

另一種技術是設定標誌變數以指示您在過程主體中的位置,然後在錯誤處理程式中編寫邏輯,根據錯誤發生的位置採取不同的操作。我通常不喜歡這種方法,但我承認我有時會使用它,因為我無法找到我更喜歡的方法。

如果您感覺我對 VB 的錯誤處理機制不太滿意,那麼您是絕對正確的。因此,除了說您應該在應用程式中實現足夠的錯誤處理以確保它正常可靠地工作外,我不會強制執行任何特定的錯誤處理樣式。如果您在過程中實現了一個錯誤處理程式,請使用前面顯示的命名約定作為行標籤。

標準錯誤處理程式

[edit | edit source]

這個想法是將其作為一種通用錯誤處理程式,各個錯誤處理例程可以呼叫它來處理許多常見的錯誤。

使用標準錯誤處理程式涉及以下步驟

  • 將包含預設錯誤處理程式的模組新增到專案中
  • 從您的錯誤處理例程中呼叫 DefErrorHandler

這是執行此操作的標準方法

  Sub DoSomething()
  
    On Error Goto DoSomething_Error
    . . .
  DoSomething_Exit:  
    Exit Sub
  
  DoSomething_Error:
    Select Case Err
      Case xxx:
      Case yyy:
      Case xxx:
      Case Else: DefErrorHandler Err, "DoSomething",_
                                 "", ERR_ASK_ABORT_
                                 Or ERR_DISPLAY_MESSAGE  
    End Select
  
    Resume DoSomething_Exit

  End Sub

請注意用於捕獲將在本地處理的錯誤的 Select Case 語句。所有需要在本地處理的預期錯誤都將具有相應的 Case 語句。各個案例可以選擇使用 Resume 語句的不同形式來控制程式的流程。任何已處理的錯誤如果沒有執行 Resume,將繼續執行到最後一條語句,該語句將導致過程優雅地退出。

任何未處理的錯誤都將傳遞給 DefErrorHandler。各個案例也可以呼叫 DefErrorHandler 來記錄訊息並顯示使用者警報。

DefErrorHandler 的第一個引數是已捕獲的錯誤號。第二個引數是一個字串,用於指示錯誤發生的位置。此字串用於訊息框和日誌檔案中。第三個引數是要寫入日誌檔案並顯示給使用者的訊息。第四個引數是一組標誌,指示過程應該執行什麼操作。有關更多資訊,請參閱 ERRORGEN 中定義的常量。

預設錯誤處理程式還可以提供退出點,您可以在其中進一步自定義通用錯誤處理程式的工作方式,而無需修改程式碼。這些將採用在處理週期的方便位置呼叫的全域性過程的形式。透過自定義這些過程的工作方式,您可以更改錯誤處理過程的外觀和功能。

以下是一些您可能想要實現的常量和過程示例

gsERROR_LogFileName
定義日誌檔案根檔名的常量
gsERROR_LogFilePath
定義日誌檔案路徑的常量,可以留空以使用 App.Path,也可以更改為變數並在程式初始化期間設定為有效的路徑
gsERROR_MSG_???
一系列常量,定義預設訊息框的某些字串、按鈕和圖示。
gsERROR_LOGFORMAT_???
一系列常量,定義日誌檔案中行的分隔符和格式
sERROR_GetLogLine
一個可以完全重新定義日誌行格式的函式
sERROR_GetLogFileName
返回日誌檔案完整路徑的函式;可以自定義此函式以允許處理日誌檔案的非固定位置(請參閱函式中的註釋)
ERROR_BeforeAbort
在程式使用 End 語句終止之前呼叫的子過程;這可用於在錯誤情況下執行任何最後一分鐘的清理工作
bERROR_FormatMessageBox
一個可以對錯誤訊息框進行自定義格式化的函式,或者可以使用自定義處理完全替換訊息框(例如,使用表單在繼續或結束之前從使用者那裡收集更多資訊)

格式化標準

[編輯 | 編輯原始碼]

程式碼的物理佈局對於確定其可讀性和可維護性非常重要。我們需要制定一套通用的程式碼佈局約定,以確保包含來自不同來源的程式碼的程式既可維護又美觀。

這些指南不是硬性規定,不像之前提到的變數命名約定那樣嚴格。和往常一樣,請根據自己的判斷,記住你的目標是建立儘可能好的程式碼,而不是死板地遵循規則。

也就是說,在偏離標準之前,你最好有充分的理由。

空白和縮排

[編輯 | 編輯原始碼]

每次縮排三個空格。我知道預設的編輯器製表符寬度是四個字元,但我更喜歡三個空格,原因有幾個。我不想縮排太多,而且我認為兩個空格實際上更節省編輯器中的螢幕空間。我選擇三個空格的主要原因是,最常見的縮排原因(If 和 Do 語句)在它們的程式碼塊與關鍵字後面的第一個字元對齊時看起來不錯。

  If nValueCur = nValueMax Then
     MsgBox . . .
  End If
  Do While nValueCur <= nValueMax
     Print nValueCur
     nValueCur = nValueCur + 1
  Loop

不幸的是,其他兩個主要的縮排原因(For 和 Select 語句)無法完全符合這種方案。

編寫 For 語句時,自然傾向於縮排四個字元。

  For nValueCur = nValueMin To nValueMax
      MsgBox . . .
  Next nValueCur

只要 For 語句不長,並且不包含進一步的縮排,我不太擔心這個問題。如果它很長,並且包含更多縮排,那麼很難使結束語句對齊。在這種情況下,我強烈建議你堅持使用標準的三個空格縮排。

別忘了,如果你更喜歡使用製表符鍵,可以設定 VB 編輯器,讓製表符鍵縮排三個空格。我個人使用空格鍵。

對於 Select Case 語句,有兩種常用的技術,這兩種技術都是有效的。

在第一種技術中,Case 語句根本不縮排,但由每個語句控制的程式碼按標準數量縮排三個空格,如下所示。

  Select Case nCurrentMonth
  
  Case 1,3,5,7,8,10,12
     nDaysInMonth = 31
  
  Case 4,6,9,11
     nDaysInMonth = 30
  
  Case 2
     If IsLeapYear(nCurrentYear) Then
        nDaysInMonth = 29
     Else
        nDaysInMonth = 28
     End If
  
  Case Else
     DisplayError "Invalid Month"
  End Select

在第二種技術中,Case 語句本身縮排少量(兩個或三個空格),而它們控制的語句則超級縮排,基本上暫停了縮排規則。

  Select Case nCurrentMonth
    Case 1,3,5,7,8,10,12:  nDaysInMonth = 31
    Case 4,6,9,11:         nDaysInMonth = 30
    Case 2                 If IsLeapYear(nCurrentYear) Then
                              nDaysInMonth = 29                 
                           Else                            
                              nDaysInMonth = 28     
                           End If
    Case Else:             DisplayError "Invalid Month"
  
  End Select

請注意,冒號如何被用來使語句出現在條件的右側。還要注意,你不能對 If 語句這樣做。

這兩種技術都是有效且可接受的。在程式的某些部分,其中一種技術會比另一種更清晰、更易於維護,因此在做出決定時請運用常識。

讓我們不要太過糾結於縮排,夥計們。我們大多數人都明白什麼縮排方式是可接受的,什麼是不合適的。

表示式中的空格

[編輯 | 編輯原始碼]

在運算子周圍和表示式中的逗號之後加一個空格。

Let miSomeInteger = 1 + 2 * (miSomeOtherInteger - 6)

註釋程式碼

[編輯 | 編輯原始碼]

這一條會非常有爭議。不要過多的註釋。現在,以下列出了一些你絕對需要註釋的地方;也可能還有其他地方需要註釋。

註釋頭塊

[編輯 | 編輯原始碼]

每個主要例程都應該有一個頭,用來標識

  • 誰編寫的
  • 它應該做什麼
  • 引數的含義(包括輸入和輸出)
  • 它返回什麼(如果是函式)
  • 它對程式或環境狀態的假設
  • 任何已知的限制
  • 修訂歷史

以下是一個函式頭的示例

  Function ParseToken(sStream_INOUT As String, _
                      sDelimiter_IN As String) As String
  '===============================================================
  'ParseToken
  '---------------------------------------------------------------
  'Purpose : Removes the first token from sStream and returns it
  '          as the function result. A token is delimited by the
  '          first occurrence of sDelimiter in sStream.
  '
  ' Author : Jim Karabatsos, March 1996
  '
  ' Notes   : sStream is modified so that repeated calls to this
  '           function will break apart the stream into tokens
  '---------------------------------------------------------------
  ' Parameters
  '-----------
  ' sStream_INOUT : The stream of characters to be scanned for a
  '                 token.  The token is removed from the string.
  '
  ' sDelimiter_IN : The character string that delimits tokens
  '---------------------------------------------------------------
  ' Returns : either a token (if one is found) or an empty string
  '           if the stream is empty
  '---------------------------------------------------------------
  'Revision History
  '---------------------------------------------------------------
  ' 11Mar96 JK  : Enhanced to allow multi-character delimiters
  ' 08Mar96 JK  : Initial Version
  '===============================================================

看起來很多,不是嗎?事實是,你不會為每個過程都寫一個這樣的頭。對於許多過程來說,在頂部新增幾行註釋就足以傳達所有需要的資訊。對於事件過程,通常根本不需要這樣的註釋塊,除非它包含大量程式碼。

然而,對於重要的過程,在你能夠寫出這樣的頭之前,你對處理過程的思考還不夠深入,無法開始編寫程式碼。如果你不知道標頭檔案中包含什麼,你就無法維護該模組。你也無法指望重用它。所以,在你編寫程式碼之前,你可以先將它輸入到編輯器中,並傳遞你的知識,而不是要求別人閱讀程式碼來破譯正在發生的事情。

請注意,標頭檔案沒有描述函式如何完成其工作。這是原始碼的作用。你不想在以後更改例程的實現時還要維護註釋。還要注意,每行註釋的末尾都沒有結束字元。不要這樣做。

  '****************************************************
  '* MY COMMENT BLOCK                                 *
  '*                                                  *
  '* This is an example of a comment block that is    *
  '* almost impossible to maintain. Don't do it !!!   *
  '****************************************************

這可能看起來很 "漂亮"(雖然這真的可以討論),但要維護這種格式非常困難。如果你有時間做這種事情,那麼你顯然不夠忙。

總的來說,為變數、控制元件、窗體和過程選擇合理的名字,再加上經過深思熟慮的註釋塊,對於大多數過程來說就足夠了。請記住,你並不是在向非程式設計師解釋你的程式碼;假設檢視程式碼的人對 VB 非常瞭解。

在過程主體中,你需要使用兩種型別的註釋:行內註釋和行尾註釋。

行內註釋

[編輯 | 編輯原始碼]

行內註釋是指單獨出現在一行上的註釋,無論它們是否縮排。

行內註釋是程式設計中的便利貼。你可以在程式碼中使用它們來添加註釋,以幫助自己或其他需要以後使用程式碼的程式設計師。使用行內註釋記錄程式碼中的以下資訊。

  • 你在做什麼
  • 你進行到哪裡了
  • 你為什麼選擇某個特定的選項
  • 任何需要了解的外部因素

以下是一些行內註釋的適當使用示例

我們在做什麼

' Now update the control totals file to keep everything in sync

我們進行到哪裡了

  ' At this point, everything has been validated.
  ' If anything was invalid, we would have exited the procedure.

為什麼我們選擇某個特定的選項

  ' Use a sequential search for now because it's simple to code
  ' We can replace with a binary search later if it's not fast
  ' enough
  ' We are using a file-based approach rather than doing it all
  ' in memory because testing showed that the latter approach
  ' used too many resources under Win16.  That's why the code
  ' is here rather than in ABCFILE.BAS where it belongs.

需要牢記的外部因素

  ' This assumes that the INI file settings have been checked by
  ' the calling routine

請注意,我們沒有記錄程式碼本身已經很明顯的內容。以下是一些行內註釋的不適當使用示例

  ' Declare local variables
  Dim nEmployeeCur As Integer
  ' Increment the array index
  nEmployeeCur = nEmployeeCur + 1
  ' Call the update routine
  UpdateRecord

註釋那些從程式碼中不容易看出來的內容。不要用英語重寫程式碼,否則你幾乎肯定無法使程式碼和註釋保持同步,這非常危險。反之,當你檢視別人的程式碼時,你應該完全忽略任何與程式碼語句直接相關的註釋。事實上,請幫大家一個忙,把它們刪除。

行尾註釋

[編輯 | 編輯原始碼]

行尾 (EOL) 註釋是附加在程式碼行末尾的小注釋。行尾註釋與行內註釋在感知上的區別在於,行尾註釋非常側重於一行或幾行程式碼,而行內註釋則指的是較大的程式碼段(有時是整個過程)。

可以將行尾註釋視為文件中的頁邊注。它們的目的是解釋為什麼需要做某件事或為什麼現在需要做。它們還可以用來記錄對程式碼的更改。以下是一些適當的行尾註釋示例

mnEmployeeCur = mnEmployeeCur + 1    ' Keep the module level
                                     ' pointer synchronised
                                     ' for OLE clients
                                     
mnEmployeeCur = nEmployeeCur         ' Do this first so that the
UpdateProgress                       ' meter ends at 100%

If nEmployeeCur < mnEmployeeCur Then ' BUG FIX 3/3/96 JK

請注意,行尾註釋可以根據需要繼續到額外的行,如第一個示例所示。

以下是一些行尾註釋的不適當使用示例

  nEmployeeCur = nEmployeeCur + 1 ' Add 1 to loop counter
  UpdateRecord  ' Call the update routine

你真的想把每個程式寫兩次嗎?


上一個:語言 內容 下一個:選定函式
華夏公益教科書