跳转到内容

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

你真的想把每个程序写两次吗?


上一个:语言 内容 下一个:选定函数
华夏公益教科书