读书频道 > 网站 > 网页设计 > Excel专业开发(第2版)
3.3.3 通用的VBA编程惯例
13-01-24    奋斗的小年轻
收藏    我要投稿   

本文所属图书 > Excel专业开发(第2版)

本书基于Excel 2003及之后的版本和VSTO.NET 2008开发平台介绍了Excel专业应用程序开发的各种方法和技术,其内容相对第一版有较大的调整和更新。主要包括Excel各类应用程序的结构和开发方法;Excel应用程序的工...立即去当当网订购

本节将介绍几个VBA编程管理的最佳方法,以帮助开发人员编写出更健壮、更易于维护与更新的代码。

1.模块使用指南

模块指南是指代码模块最开始的声明部分,用以指导VBA如何处理该模块中的代码。尽管这些指南不是必需的,但至少应该使用下面所列的一项到两项:

Option Explicit在所有模块中都应使用这一声明。在实际编程的过程中无论怎样强调其重要性都不为过。因为,如果不使用这一声明,则VBA会将因拼写失误所产生的错误名称定义为一个新变量。这类错误在运行时可能不会立即出现,具有很强的隐蔽性。当然,由于它的存在必然会使程序产生错误的结果。如果不仔细检查就可能会忽略这类错误,或许要等到将程序发布给用户之后才能发现其中的问题。

Option Explicit表示所有变量必须先进行显式声明,然后才能使用。当单击VBE中的“调试 >编译”菜单时,Option Explicit可让VBA一旦遇到不能识别的标识名,就自动返回一个编译错误,这样能比较容易地发现拼写错误。单击VBE中的“工具>选项”菜单,在弹出的“选项”对话框中选择“编辑器”属性页,勾选“要求变量声明”的复选框,即可将Option Explicit自动填写在每个模块的顶部。在此,强烈推荐采用这种设置。

Option Private Module 如果在模块中使用了Option Private Module声明,则用户接口或其他的Excel工程均不能使用其中用到的所有过程。此项设置可隐藏模块中的过程,使外界的应用程序无法访问。

提示:方法Application.run可使Option Private Module声明失效,运行模块中声明为私有的过程。此外,如果用户知道过程的准确名称,并且过程运行时无须任何参数,那么用户可以直接在宏对话框中输入过程名称,通过手动的方式执行该过程。这种情形不适合用于保护工程,因为私有过程名在对象浏览器中是不可见的。

Option Base 1该声明可将所有未指定下界的数组变量的下界值设置为1。通常,不要使用Option Base 1声明,而应该明确指定数组变量的上下边界值。因为,若某个模块采用Option Base 1的声明,则将其中的过程复制到其他没有采用该声明的模块中时,可能会发生错误。由此可见使用Option Base 1声明会影响程序的可重用性。

Option Compare Text该声明表明在比较模块中的字符串时应采用基于文本而不是二进制的标准。所谓基于文本的比较标准就是不区分字母的大小写,而基于二进制的比较标准则要区分字母的大小写。同Option Base 1一样,应尽量避免使用Option Compare Text声明,因为它可能造成同一个过程在采用和有采用Option Compare Text声明的模块中表现出不同的行为特性。基于文本的字符串比较比基于二进制的字符串比较计算开销更大,所以使用Option Compare Text会影响模块中字符串比较的性能。大部分Excel和VBA的字符串比较函数都提供了相应的参数,用于设置是进行基于文本还是基于二进制的字符串比较。除非特别要求,否则应当选用基于参数设置方式来控制字符串的比较。

在某些少数情况下需要使用Option Compare Text。最常见的情况是在VBA中使用 like运算符进行不区分大小写的字符串比较时,必须使用Option Compare Text声明。在这种情况下,应该将使用Option Compare Text声明的过程划分到一个单独的模块中,以便让不使用该声明的其他过程不受影响。同时,在模块中添加相应的注释语句说明这么做的理由。

2.变量和常量的使用惯例

变量和常量是应用程序中最基本的构成单元,我们对它们非常熟悉,以至于有时甚至忽略了必须要正确地使用这些应用程序中的活动单元,否则会影响程序的质量。本节将介绍变量和常量的一些使用惯例。

(1)避免重复使用变量

程序中所声明的变量应该只有一个特定目的。将同一变量用于多种目的时,节约的仅仅是声明变量的语句行,但却会给程序带来大量的潜在问题。比如在查看过程是如何工作时,常常需要确定某些特定区域的变量值,当某个变量第二次出现时,我们自然会认为它所代表的意义与第一次出现时相同。若情况不是这样,即一个变量有多种意义,显然会增加理解程序逻辑的难度。

(2)避免使用变体数据类型

尽可能不使用变体数据类型。VBA并非强类型编程语言,如果在声明变量时未指定其数据类型,系统会自动指定为变体类型。尽量不使用变体类型的理由如下:

变体类型的效率较低 在VBA编程语言中,变体类型是一种非常复杂的结构,用来存放任何类型的数据。变体类型不像其他基本数据类型,如长整型或双精度类型,可以被直接访问和修改。相反,如果要对变体类型数据执行某种操作,VBA必须在后台执行一系列复杂的Windows API调用。

存储在变体类型中的数据具有不确定性 由于变体类型可以存储任意类型的数据,因此,存入变体时的数据类型可以与从变体中取出时不相同。在访问变体类型中的数据时,默认情况下VBA会将该数据转变为当前条件下它认为最适宜的数据类型。建议在必须使用变体类型时,用相应的VBA函数(如CStr、CLng、CDate等)显式将其转换成所需要的数据类型。

变体类型的一个有用功能是,当通过Range.Value为变体类型分配一个多单元格区域时,系统会自动生成一个包含该区域所有值的二维数组。当需要处理包含在一个单元格区域中的大量数据时,变体类型能很快地将数据转存为一个变体数组,执行相应的操作并实现数组循环,最后再将变体数组中的值保存回单元格区域中。代码清单3-11给出了实现上述操作的实例。

代码清单3-11  利用变体数组操作区域值
Sub UseVariantArray()

Dim lRow As Long
Dim lCol As Long
Dim vaArray As Variant

vaArray = Sheet1.Range("A1:E5").Value

For lRow = LBound(vaArray, 1) To UBound(vaArray, 1)
          For lCol = LBound(vaArray, 2) To UBound(vaArray, 2)
           vaArray(lRow, lCol) = vaArray(lRow, lCol) * 2
          Next lCol
Next lRow

Sheet1.Range("A1:E5").Value = vaArray

End Sub

(3)小心错误的类型转换

VBA并非一种强类型语言,这是产生错误类型转换的最主要原因。当VBA自动将一种数据类型转换为其他不相关的数据类型时,就可能会发生错误的类型转换。常见的情况如:将存储数字的字符串转换为整数,或将逻辑值转换为等同的字符串。在VBA程序代码中使用变量时一定要弄清楚变量的类型,不要随意混用,应显式地调用转换函数(如CStr、CLng、CDate等)进行类型转换,以告知VBA该如何处理这些变量。

(4)避免使用 As New声明语法

切记不要使用As New来声明对象变量。比如,决不要使用如下所示对象声明方式:

Dim rsData As New ADODB.Recordset

如果VBA在某句代码中遇到像这样声明的变量,且变量尚未初始化,那么VBA会自动对其进行初始化,创建一个相应的对象实例。然而这决非我们所需要的。在好的编程惯例中,应该由程序员完全控制对象的创建和使用。如果VBA在代码中发现了某个未初始化的对象,几乎可以断定这段代码存在缺陷,并且系统应立即报告该信息。为此,应该按照如下的方式来声明和初始化对象:

Dim rsData As ADODB.Recordset

Set rsData = New ADODB.Recordset

当采用这种声明和初始化的方式时,若在过程中的某处销毁了对象变量,且在之后的其他地方又再次引用了该对象变量,则VBA会马上抛出运行错误“Object variable or With block variable not set”,以提示程序开发人员。

(5)尽量使用完整的对象名称

在程序代码中声明变量时,应尽量使用包含类前缀的完整的对象名称。因为有些对象名称在很多对象库中都存在。如果声明对象时仅仅单独使用对象名称,且在应用程序所引用的多个对象库中都含有此对象名称,那么VBA会根据哪一个对象库来创建对象变量呢?此时,VBA将以“工具>引用”列表中第一个能找到该对象名称的类库为标准来创建对象变量。通常条件下,这种处理方式并不是我们所期望的。

现在以用户窗体控件为例,说明由于没有使用完整的对象名称而引发的问题。假设需要声明一个对象变量来引用用户窗体中的文本框控件,可能使用的代码为
Dim txtBox As TextBox
Set txtBox = Me.TextBox1

不幸的是,当VBA执行第二行代码时,就会产生“类型不匹配”的错误。这是因为Excel对象库中也包含一个TextBox对象,同时在“工具>引用”列表中Excel对象库又位于MSForms对象库的前面。所以,正确的代码应为
Dim txtBox As MSForms.TextBox
Set txtBox = Me.TextBox1

(6)不要使用固定数字作为数组的边界

在使用循环方式访问数组时,不要使用固定数字作为数据的边界,而应借助函数LBound和UBound来完成,相关示例如代码清单3-12所示。

代码清单3-12  循环访问数组的正确方式
Dim lIndex As Long
Dim alListItems(1 To 10) As Long

' Load the array here.

For lIndex = LBound(alListItems) To UBound(alListItems)
     ' Do something with each value.
Next lIndex

因为在应用程序的创建和维护过程中,数组的边界值经常会发生变化。如果在代码清单3-12的循环语句中使用固定数字1和10作为数组的边界,那么每当数组alListItems的边界发生变化时,都需要更改这一循环,否则程序就会发生错误。而使用函数LBound和UBound,可使循环具有很好的自适应性。

(7)在Next语句后总是指定循环的计数变量

从代码清单3-12中,还可看到另一种好的编程习惯,即在Next语句后指定循环的计数变量。尽管这并非VBA的强制要求,然而这样做可使代码更容易理解,特别是在For和Next语句相隔较远时更是如此。

(8)充分利用常量

常量是一种非常有用的编程元素。在代码中,它们常用于以下方面:

利用常量来代替“魔数”,即用一些易于理解的名称来代替数字。比如在下面的代码中,数字50表示什么意思呢?
If lIndex < 50 Then

除非你是代码的设计者,能记得50的含义,否则从代码本身是无法知道的。

而如果引入常量,采用下面的方式编写代码,那么其含义就容易理解了。
Const lMAX_NUM_INPUT_FILES As Long = 50

' More code here.

If lIndex < lMAX_NUM_INPUT_FILES Then

在设计时,如果需要查看常量的值,那么在VBE环境中,可在常量名上单击鼠标右键,从弹出的快捷菜单中选择“定义”,这样就能直接跳转到代码中常量的定义处。如果要在运行时的中断模式下查看常量的值就更加容易,只需将鼠标放到常量位置上,系统就会弹出一个包含常量值的提示标签。

使用常量可以提高代码的效率,并能避免因删除副本数据而引起的错误。在前面的示例中,假设在应用程序中会多次使用“输入文件的最大数量”这个值。在某个时刻,可能会对应用程序进行升级,以便能处理更多的输入文件。如果应用程序中是直接使用数字来表示“输入文件的最大数量”,那么将不得不对程序中凡是用到该数字的地方都逐一进行修改,一旦修改不彻底就会产生错误。而如果是通过常量来表示“输入文件的最大数量”,那么仅仅需要在常量定义处进行修改即可。显然,上面所说的因直接使用数字而引起的修改不彻底错误可通过常量很好地解决。

使用公有变量应该特别注意,因为在应用程序的任何地方都可以直接修改它,且系统不会给出任何提示或警告信息。同时,公有变量的这种访问方式也破坏了一个重要的编程原则:封装性。因此在创建变量时应使其作用域尽可能小。建议在开始创建变量时将其均设置为局部变量,仅在确实需要时才扩大其作用范围。

凡事总有例外,在某些情况下确实需要使用公有变量,如:

一些在使用前必须被压入栈底的变量。假设过程A读取了某个数据,然后将其传给过程B,过程B又将其传给过程C,过程C又将其传给过程D,最终在过程D中使用此数据。针对这种情况,最好的办法是将该数据表示为公有变量,然后由过程A直接传给过程D。

某些固有的公有类。比如应用程序级的事件处理类,将其表示为公共的对象变量就可在应用程序的任何地方使用。

(10)早期绑定与后期绑定

对早期绑定与后期绑定,许多人对其区别的理解常常有误,也没有完全弄清楚其中对象是如何创建的。其实,判定一个对象是早期绑定还是后期绑定只需关注一个因素,即引用该对象的对象变量是如何声明的。若对象变量声明为某种特定的对象数据类型,属于早期绑定。若对象变量声明为Object或变体数据类型,则属于后期绑定。代码清单3-13和3-14分别为后期绑定和早期绑定的例子。

代码清单3-13  通过后期绑定引用 ADO Connection对象
Dim objConnection As Object

' It doesn't matter how you create the object, it's still
' late bound due to the As Object variable declaration.
Set objConnection = New ADODB.Connection
Set objConnection = CreateObject("ADODB.Connection")

代码清单3-14  通过早期绑定引用ADO Connection对象
Dim cnConnection As ADODB.Connection

' It doesn't matter how you create the object, it's still early
' bound due to the data type used in the variable declaration.
Set cnConnection = New ADODB.Connection
Set cnConnection = CreateObject("ADODB.Connection")

注意,如果要对Excel对象库之外的对象使用早期绑定,必须先在VBE的菜单“工具>引用”中指定一个对相关对象库的引用。例如,要创建一个早期绑定ADO对象的变量,必须首先指定一个对Microsoft ActiveX Data Objects 2.x库的引用(此处x表示ADO的版本号),应该尽可能地使用早期绑定。相对后期绑定而言,早期绑定具有以下优点:

性能更好  编译时,若VBA已知对象变量的数据类型,则VBA可从对象中获取其属性和方法的内存地址,并将其与代码存储在一起。在运行时,如果VBA遇到了早期绑定的属性或方法,只需要执行对应存储位置的代码即可。(为了说明方便,在描述上做了一定程度的简化。实际上VBA存储的是执行代码相对于内存起始地址的偏移量,即相对于对象Vtable的偏移量。)

若变量为后期绑定,则VBA无法知道变量所包含的对象类型。所以,在编译时就不能对属性及方法进行优化。这意味着若VBA需要调用后期绑定变量的属性和方法,每次都必须先确定变量所包括的对象类型,然后再根据属性或方法的名称确定其对应的内存位置,最后才执行相应的代码。显然,这个过程比早期绑定慢得多。

类型检查更严格 在代码清单3-13所示的后期绑定示例中,如果不小心将对象变量所引用的ADO Connection对象写成了ADO Command对象,VBA不会就此给出提示。你只有在代码中使用到Command对象不支持的方法和属性时,才会发现其中潜在的问题。然而,如果对象采用的是早期绑定的方法,那么VBA会立即检测出当前所引用的对象有误,并给出“类型不匹配”的错误提示。这样可在代码尚未运行时就检测出所调用属性或方法的不正确性。在编译时,VBA会在对应的对象库中查找要调用的属性名或方法名,如果没有找到,VBA就会抛出编译时错误。

具有智能感知的功能 采用早期绑定的方式可使编程更为容易,因为VBA能确切地知道变量所代表的对象类型,在用户输入属性或方法名称的一部分时,VBA就会解析相应的对象库,并弹出一个下拉列表框,其中包含了相关的属性名和方法名,方便用户选择。

然而,在某些特殊情况下需要使用后期绑定而非早期绑定。以下是两个最常见的必须使用后期绑定的情况:

1.采用早期绑定的方式时,若程序中需要的对象库版本比计算机中现有的版本更高,则程序的兼容性会被破坏,无法正常运行。

这种情况非常普遍。如果应用程序中引用的是新版的应用程序对象库,而当前运行程序的计算机中采用的却是旧版的应用程序对象库,此时就会产生“无法找到工程或库(Can't find project or library)”的运行错误。而且,这种情况非常容易引起误导,因为产生这种错误的根本原因是代码编写不当,而与对象库没有任何关系。

如果希望避免上述错误的出现,让应用程序在各种版本的对象库中均能正确运行,就需要在引用对象时使用后期绑定。在创建新对象时,也只能使用带有独立参数ProgID的函数CreateObject,而不能使用“=New ObjectName”的语法格式。

2.当不能确定用户是否安装了应用程序所需要的支持软件,而且也不能自行安装这些软件时,只能采用后期绑定的方式。

在这种情况下,采用后期绑定的方式可以避免因为没有相应的对象库而产生的编译错误。在应用程序运行时,它会检查在当前计算机中该对象库是否存在,如果不存在,应用程序就会自动退出。

提示:在编写使用后期绑定的代码时,可以先用早期绑定代替后期绑定,这样可充分利用代码的自动提示,提高编写的效率。而在代码编写完毕后,在最后的测试和发布之前再将早期绑定更改为后期绑定。

3.预防性编代码

预防性的编码是指各种用于预防错误产生,而非错误发生后再进行纠正的编程习惯及其代码。

(1)尽量在早期的Excel版本上设计应用程序

尽管微软的Excel小组在向前兼容方面做了大量卓有成效的工作,但在各版本之间仍然存在不少细微的区别。如果设计者非常熟悉Excel较新的版本,那么其所设计的程序往往不能在早期的Excel版本上运行,因为应用程序中很可能会使用只有最新版Excel才支持的特性或功能。

避免出现上述问题的最佳方法就是尽量在早期版本的Excel中设计应用程序。可以采取以下做法,其效果依次逐渐改善。

在同一计算机上安装多个Excel版本(不推荐)。

在多台独立的计算机上分别安装不同的Excel版本。

使用VMware或Virtual PC等虚拟化软件来保证在同一台计算机上同时维持多个独立的开发环境。

假设设计者是在Excel 2003环境下开发的应用程序,之后发现其在Excel 2000环境下不能正常运行,此时设计者必须对程序进行调试,删掉Excel 2000所不支持的代码。而如果一开始就在Excel 2000环境中开发应用程序,则会节省不少的时间和精力。

(2)显式使用ByRef或ByVal

当过程包含参数时,通常有两种方式来声明这些参数,即ByRef和ByVal

ByRef 表示将变量的地址而非变量值传递给过程。如果在过程中修改了ByRef参数的值,则在该过程调用完成后相应的修改在主调过程中仍然存在。

ByVal 表示将变量的值传递给过程。即使在过程中修改了ByVal参数的值,在该过程调用完毕后这些修改也不会影响到主调过程。实际上,过程是将ByVal参数作为局部变量来处理。

在过程中应该总是显式地声明参数为ByRef或ByVal。如果未显式声明,则在默认条件下,系统会将参数作为ByRef类型。除非特别需要在调用过程后保留参数值的更改情况,否则应该用ByVal来声明参数。将参数声明为ByVal可防止其改变传递回调用的过程。

但是也存在一些特例,比如向过程传递长字符串时,应该使用ByRef的方式,因为其效率更高。或者当过程中的参数是某种特殊的类型时,比如数组,也应该采用ByRef的方式。注意,将过程参数声明为ByVal会更容易发现强制类型转换的错误。如果过程参数为ByRef,则在调用过程时,必须为该参数传递其所声明的数据类型,否则会产生编译错误。而在传递ByVal类型的过程参数时,VBA会将参数值强制转换为兼容的数据类型。

(3)显式调用对象的默认属性

由于集合对象的元素属性可能会发生改变,因此在表达式中不要使用对象名来表示对象默认的属性。在代码清单3-15中,分别给出了一个正确和错误使用控件MSForm.TextBox默认属性的例子(MSForm.TextBox 控件的默认属性是Text)。

代码清单3-15  默认属性
' The right way.
txtUsername.Text = "My Name"

' The wrong way
txtUsername = "My Name"

为了让代码更具有可读性,应该避免隐式使用对象的默认属性,这样还可防止将来因对象版本升级后默认属性发生变化而引起的错误。

(4)在使用前验证过程参数的有效性

如果过程需要接受外界的输入,就必须采取一定的措施来检验这些输入的正确性。比如,输入值必须在某个确定的范围内。只有当输入值通过有效性检验后才能运行过程。采用这种处理方式,能尽早发现输入中的错误,给出相应的提示信息,同时还能简化过程的调试。

根据需要,创建相应的桩模块来检验程序的有效性。所谓桩模块实际上是一个封装的程序,用于调用需要测试的程序,向其传递一系列的参数,并能明确的显式测试结果是否正确。在第16章“VBA的调试”中将会详细介绍用于测试的桩模块。

(5)通过保护计数器来防止无限循环

此处主要讨论如何防止无限循环的问题。在使用Do…While或While…Wend循环结构时最常见的错误是循环结束条件永远不成立,即发生了“死循环”。(如果幸运,或许可以使用Ctrl+Break组合键来强行中止程序,否则可通过任务管理器来强制结束程序)。为了防止“死循环”的发生,可在循环中引入一个保护计数器,当循环次数超过设定的最高值时(一般不会发生这种情况),就立即结束循环。代码清单3-16为一个在Do...While循环结构中使用了保护计数器的示例。

代码清单3-16  通过保护计数器来防止无限循环
Dim bContinueLoop As Boolean
Dim lCount As Long

bContinueLoop = True
lCount = 1

Do

 ' The code that goes here should set the
 ' bContinueLoop variable to False once the
 ' loop has achieved its purpose.
 
 ' This infinite loop guard exits the loop
 ' with an error after 10000 iterations.
 lCount = lCount + 1
 If lCount > 10000 Then Err.Raise _
  Number:=9999, Description:="Infinite Loop Error!"

Loop While bContinueLoop

循环结构中变量lCount的唯一作用是:如果在10 000次迭代后,控制变量仍不能满足循环退出的条件,就强制退出循环。

(当然循环次数最大为多少,应根据具体应用环境来设定。)这种处理方式使循环结构显得稍稍复杂一些。在确保程序不会发生无限循环的条件下,可以将保护计数器删除或注释停用。

(6)尽早并经常点击“调试>编译”菜单项

千万不要在程序进行了大量的修改和确保无误之后才点击“调试>编译”菜单项,这样会导致调试过程过于漫长和低效。

(7)引用工作表对象时使用其代码名称

注意在程序中总使用代码名称来引用工作表和图表。通过表格的标签名来识别表格是非常不安全的,因为用户很可能会修改表格的标签名,从而导致使用标签名的代码无法工作。

(8)验证选定项的数据类型

如果过程是专门用于处理某种特定数据类型的对象,那么必须使用TypeName函数或If Typeof…Is结构来检查用户所选定项的数据类型是否正确。比如,假设过程是工作在Excel的某个范围内,那么在开始工作前,必须首先确保用户所选定的是Range对象。具体代码如代码清单3-17所示。

代码清单3-17  检验选定的对象是正确的类型
' Code designed to operate on a range.
If TypeOf Selection Is Excel.Range Then
     ' OK, it's a Range object.
     ' Continue code execution.
Else
     ' Error, it's not a Range object.
     MsgBox "Please select a range.", vbCritical, "Error!"
End If

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站