乐于分享
好东西不私藏

Excel VBA 编程基础 -- 过程与函数(一)

Excel VBA 编程基础 -- 过程与函数(一)
从今天开始我们讨论 VBA 中最重要的程序结构——过程与函数。
其实,过程与函数我们已经见过不少,我们在代码窗口中写的代码都是处于 Sub ... End Sub 结构的包围之中,而且我们已经用过很多函数了,譬如日期时间的格式化函数 FormatDateTime,等等。
现在要讨论的是如何写我们自己的过程和函数。
先说过程。
过程(Sub)
过程是一个组合 VBA 语句的手段,譬如我们录制的宏最后就保存在一个过程中。过程的一般结构是:
[ modifier ] Sub SubName([ parameters ])
[ statements ]
Exit Sub ]
[ statements ]
End Sub
其中,modifier 我们以后再讨论。parameters 是过程的参数,一个过程可以带有 0 个或多个参数,多个参数用逗号(,)隔开。
让我们从例子入手,来逐步熟悉过程的写法。
例1. 一个简单的过程例子
Sub ComputeCircleArea(radius)  Dim area AsDouble  If radius <=0Then    Exit SubEnd If  area = WorksheetFunction.Pi() * radius * radius  Debug.Print "Area: " & areaEnd Sub
在这个例子中,过程名称为 ComputeCircleArea,意思是计算圆面积,这个过程需要一个参数:radius,即圆的半径。
接下来,我们声明了一个本地变量(local variable)area,这是一个双精度浮点型的变量。这个变量之所以称为“本地变量”,是因为此变量只在这个 ComputeCircleArea 过程内才有意义,在这个过程之外访问这个变量是没有意义的,除非在这个过程之外还有一个名为 area 的变量,但即使存在一个这样的变量,那也和我们现在声明的这个 area 没有任何关系。
然后,我们判断参数 radius 的合法性,如果这个参数小于等于零,则对于我们这个过程来说没有意义,所以什么也不做,执行 Exit Sub 语句退出当前过程。
如果参数 radius 有意义,则利用圆面积的计算公式计算圆的面积。注意,因为计算圆的面积需要常数 Pi (π),因此我们使用 WorksheetFunction.Pi() 函数来利用 Excel 中的常数 Pi (π)
计算完成后,使用 Debug.Print 打印出结果。
完整代码及运行结果如下图所示:
图1 ComputeCircleArea
这个例子包括两个过程:AreaTest 和 ComputeCircleArea。其中,ComputeCircleArea 是一个计算圆面积的过程,而 AreaTest 则是用来测试这个过程的。
我们看到,在过程 AreaTest 中,我们用三个不同的 radius 值调用(Call)过程 ComputeCircleArea,只有 radius 为 12.5 时计算出了圆面积,其他两个参数值因为不合法,所以在 ComputeCircleArea 过程中中途就返回(Exit Sub)了,没有计算结果。这与我们的设计目标是一致的。
过程调用
在过程 AreaTest 中,我们可以使用以下两种方式来调用过程 CoomputeCircleArea:
  • Call ComputeCircleArea(radius)
  • 直接调用
    • ComputeCircleArea(radius)
    • ComputeCircleArea radius
可以使用 Call 语句后跟所要调用的过程名及其参数,也可以直接使用过程名加参数的方式。VBA 文档称,如果使用 Call 方式,过程的参数表必须用括号括起来;如果不使用 Call 方式,则过程的参数表必须是无括号的。但实际测试表明,在非 Call 方式下,用括号括起参数表也是可以的。
过程调用涉及到参数传递,就我们这个例子来说,就是 radius 这个参数怎么样从 AreaTest 过程传递到 ComputeCircleArea 过程。VBA 支持两种参数传递方式:
  • ByRef:这是 VBA 的默认传递方式。在这种传递方式中,参数 radius 从 AreaTest 过程传递到 ComputeCircleArea,传递的是 radius 在 AreaTest 过程中的地址。
  • ByVal:这种传递方式传递的是参数的值。这也是图1 中的 ComputeCircleArea 过程声明的方式。
采用什么参数传递方式,取决于过程定义时对参数的声明,而与对该过程的调用无关,即调用过程时是不能指定参数传递方式的。
传值(ByVal)的实现方式
AreaTest 过程中,在调用 ComputeCircleArea 之前,VBA 会把 radius 的值拷贝到 ComputeCircleArea 过程中的 radius 变量中去,然后程序的控制会转到 ComputeCircleArea 过程,执行其中的代码。
按照早期编程语言的术语,AreaTest 过程中的 radius 称为“实参”(argument),ComputeCircleArea 过程中的 radius 称为“形参”(parameter)
所谓“实参”,就是实际参数。所谓“形参”,就是形式参数。在我们的例子中,碰巧这两个参数有同样的名字,一般情况下名字是不相同的。
在调用过程时,调用方(即 AreaTest 过程)要提供具体的参数值,因此称为“实参”,实际参数。而被调用方(即 ComputeCircleArea)要提供相应的变量来接受调用方传来的实参,这个变量就是“形参”。
由于传值的这种拷贝的实现方式,使得实参和形参各自拥有一份参数值的拷贝,但相互独立,互不影响。在被调用方的过程中,即使修改了形参的值,也不会影响到调用方的实参。
使用传值的场合:
  • 对于基本类型,易使用传值方式,效率高
  • 需要确保实参不会受到形参修改的影响
传地址(ByRef)的实现方式
如果 ComputeCircleArea 过程使用文章开头给出的版本,则参数的传递方式就是传地址。AreaTest 中,调用 CopmuteCircleArea 之前,VBA 会将 radius 的地址传递给 ComputeCircleArea 的 radius,然后控制转移到 ComputeCircleArea,执行其中的代码。
在这种传递方式中,由于形参 radius 接受的是实参 radius 的地址,因此形参和实参指向同一个地址。此时,如果在 ComputeCircleArea 中有对 radius 的赋值语句,则这个对形参的修改会影响到实参,因为两者指向同一个对象。
这种传递方式的一大优点是:对于大型数据结构,譬如数组,因为传递的是数组的首地址,而不是拷贝整个数组,因此参数传递的效率极高。
使用传地址的场合:
  • 大型数据结构作为参数传递,易使用传地址方式,效率高
  • 需要形参的修改能对实参产生影响
由此可以看出,传值和传地址是两种互为补充的关系。
例2. 参数不同传递方式的演示
图2 参数的不同传递方式
在这个例子中,我们写了一个传地址版本的 ComputeCircleArea2,传值版本的 ComputeCircleArea 与 ComputeCircleArea2 的唯一差别就是参数的传递方式。
在测试程序 AreaTest2 中,分别用不同的实参值调用两个版本的计算圆面积过程。从右侧的立即窗口我们可以看到,在传值版本中,对形参 radius 的修改并不影响调用过程中的实参 r。而在传地址版本中,对形参 radius 的修改反映到了调用过程中的实参 r 上:r 的值随着 radius 的修改也修改了,因为此时 r 和 radius 指向同一对象。