VBA allows two kinds of procedures: functions and subroutines. The only difference between a function and a subroutine is that a function returns a value, whereas a subroutine does not.
A function declaration has the form:
[Public or Private] FunctionFunctionName
(Param1
AsDataType1
, _Param2
AsDataType2
,...) AsReturnType
Note that we must declare the data types not only of each parameter to the function, but also of the return type. Otherwise, VBA declares these items as variants.
I will discuss the optional keywords Public
and Private
later in this chapter, but you can
probably guess that they are used here to indicate the scope of the
function, just as they are used in variable declarations. For example,
the AddOne
function in Example 11-1 adds 1 to the
original value.
Public Function AddOne(Value As Integer) As Integer AddOne = Value + 1 End Function
To use the return value of a function, we just place the call to the function within the expression, in the location where we want the value. For instance, the code:
MsgBox "Adding 1 to 5 gives: " & AddOne(5)
produces the message box in Figure 11-1, where the
expression AddOne(5)
is replaced
by the return value of AddOne
,
which in this case is 6.
Note that, in general, any parameters to a function must be enclosed in parentheses within the function call.
In order to return a value from a function, we must assign the function’s name to the return value somewhere within the body of the function. Example 11-2 shows a slightly more complicated example of a function.
Function ReturnCount( ) As Variant ' Return count of records in recordset If rs Is Nothing Then ReturnCount = "No recordset" Else ReturnCount = rs.RecordCount End If End Function
This function returns a count of the number of records in the
recordset referenced by the variable rs
.
However, if rs
does not currently reference
a recordset, then the function returns the words "No recordset"
.
Note that since the return value may be a number or a string, we
declare the return type as Variant. Note also that
ReturnCount
is assigned twice within the
body of the function. Its value, and hence the value of the function,
is set differently depending upon the value returned by the If
statement.
A subroutine declaration has the form:
[Public or Private] SubSubroutineName
(Param1
AsDataType1
, _Param2
AsDataType2
,...)
This is similar to the function declaration, with the notable
absence of the As
ReturnType
portion. (Note also the word
Sub
in place of Function
.)
Since subroutines do not return a value, they cannot be used within an expression. To call a subroutine named SubroutineA, we can write either:
Call SubroutineA(parameters, . . .)
or simply:
SubroutineA parameters, . . .
Note that any parameters must be enclosed in parentheses
when using the Call
keyword, but
not otherwise.
Consider the following very simple subroutine, which does nothing more than display a message box declaring a person’s name:
Sub DisplayName(sName As String) MsgBox "My name is " & sName End Sub
To call this subroutine, we would write, for example:
DisplayName "Wolfgang"
or:
Call DisplayName("Wolfgang")
The variable sName
in the procedure
declaration:
Sub DisplayName(sName As String)
is called a parameter of the procedure. The call to the procedure should
contain a string variable or a literal string that is represented by
the variable sName
in this procedure (but
see the discussion of optional arguments in the next section). The
value used in place of the parameter when we make the procedure call
is called an argument . Thus, in the previous example, the argument is the
string “Wolfgang.”
Note that many programmers fail to make a distinction between parameters and arguments, using the names interchangeably. However, since a parameter is like a variable and an argument is like a value of that variable, failing to make this distinction is like failing to distinguish between a variable and its value!
In VBA, the arguments to a procedure may be specified as
optional, using the Optional
keyword. (It makes no sense to
say that a parameter is optional; it is the
value that is optional.)
For instance, the definition of the OpenRecordset method is:
Setrecordset
=object
.OpenRecordset(source
,type
,options
,lockedits
)
where type
, options
, and lockedits
are
optional. Thus, for instance, each of the following lines of code
are legal:
Dim rs As Recordset Set rs = CurrentDb.OpenRecordset("Names") Set rs = CurrentDb.OpenRecordset("Names", dbOpenForwardOnly) Set rs = CurrentDb.OpenRecordset("Names", dbOpenForwardOnly, dbReadOnly) Set rs = CurrentDb.OpenRecordset("Names", dbOpenForwardOnly, _ dbReadOnly, dbOptimistic)
To define a function with optional arguments, we just include
the keyword Optional
in the
parameter declaration, as in Example 11-3.
Sub ChangeFieldType(sFieldName As String, _ Optional NewSize As Variant) ' Change type to integer rs!Fields(sFieldName).Type = dbInteger ' If size supplied, use it. Else use 25. If Not IsMissing(NewSize) Then rs!Fields(sFieldName).Size = CInt(NewSize) Else rs!Fields(sFieldName).Size = 25 End If End Sub
The second parameter is declared with the Optional
keyword. Because of this, we may
call the procedure with or without an argument for this parameter,
as in:
ChangeFieldType("Age", 10)
and:
ChangeFieldType("Age")
Note that the IsMissing
function is used in the body of the procedure to test whether the
argument is present. If the argument is present, then the font size
is changed. Note also that we declared the
NewSize
parameter as type Variant because
IsMissing
works only with
parameters of type Variant. (Other types of variables are given
default values, which precludes the possibility of them going
missing.) Thus, we converted the Variant to type Integer using the
CInt
function.
Note that a procedure may have any number of optional arguments, but they must all come at the end of the parameter list.
Normally, the arguments to a function are matched to the parameters by their position in the function call. For instance, in the function call:
Set rs = CurrentDb.OpenRecordset("Objects", dbOpenForwardOnly)
Access can tell that the argument dbOpenForwardOnly
is the value for the
second parameter (Type
) of the function.
Such arguments are called positional arguments
.
Many built-in VBA/DAO functions also allow named
arguments. For example, the OpenRecordset
function can be called as
follows:
Set rs = CurrentDb.OpenRecordset(Name:="Objects", _ Type:=dbOpenForwardOnly)
Here, each argument has the form:
ParameterName:=Argument
There are three main advantages to named arguments:
Named arguments can improve readability and clarity.
Blank spaces (separated by commas) are required for missing optional arguments when using a positional declaration, but not when using named arguments.
The order in which named arguments are listed is immaterial, which, of course, is not the case for positional arguments. For instance, the previous function call could be written:
Set rs = CurrentDb.OpenRecordset(Type:=dbOpenForwardOnly, _ Name:="Objects")
Named arguments can improve readability quite a bit, and they are highly recommended. However, they can require considerably more space, so for the short examples in this book, I usually will not use them.
Parameters come in two flavors: ByRef
and ByVal
. Many
programmers do not have a clear understanding of these concepts, but
they are very important and not that difficult to understand.
To explain the difference, I present the two procedures in
Example 11-4.
ProcedureA simply sets the value of the
module-level variable x
to 5, displays
that value, calls the procedure AddOne
with the argument
x
, and then displays the value of
x
again.
Sub ProcedureA( ) x = 5 ' Set x to 5 MsgBox x ' Display x Call AddOne(x) ' Call AddOne MsgBox x ' Display x again End Sub Sub AddOne(ByRef i As Integer) i = i + 1 End Sub
Note the presence of the ByRef
keyword in the AddOne
procedure declaration. This keyword
tells VBA to pass a reference to the variable
x
to the AddOne
procedure. Therefore, the AddOne
procedure, in effect, replaces its
parameter i
by the variable
x
. As a result, the line:
i = i + 1
effectively becomes:
x = x + 1
So, after AddOne
is
called, the variable x
has the value
6.
On the other hand, suppose we change the AddOne
procedure, replacing the keyword
ByRef
with the keyword ByVal
:
Sub AddOne(ByVal i As Integer) i = i + 1 End Sub
In this case, VBA does not pass a reference to the variable
x
, but rather it passes its value. Hence,
the variable i
in AddOne
simply takes on the value 5.
Adding 1 to that value gives 6. Thus, i
equals 6, but the value of the argument x
is not affected! Hence, both message boxes will display the value 5
for x
.
ByRef
and ByVal
both have their uses. When we want
to change the value of a variable, we must declare the corresponding parameter
as ByRef
so that the called
procedure has access to the actual variable itself. This is the case
in the previous example. Otherwise, the AddOne
procedure does absolutely nothing,
since the local variable i
is
incremented, but it is destroyed immediately afterwards, when the
procedure ends.
On the other hand, when we pass an argument for informational
purposes only, and we do not want the argument to be altered, it
should be passed by value, using the ByVal
keyword. In this way, the called
procedure gets only the value of the
argument.
There is one downside to passing arguments by value: it can take a lot of memory (and time). When passing a string variable that contains a large string by value, the entire string must be duplicated.
Thus, we can summarize by saying that if we want the procedure to modify an argument, the argument must be passed by reference. If not, the argument should be passed by value unless this will produce an unacceptable decrease in performance, or unless we are very sure that it will not get changed by accident.
It is important to note that VBA defaults to ByRef
if we do not specify otherwise. This
means that the values of arguments are subject to change by the
called procedure, unless we explicitly include the keyword ByVal
. Caveat scriptor
!
VBA provides the Exit
Sub
and Exit
Function
statements, should we wish to exit
from a procedure before the procedure would terminate
naturally. For instance, if the value of a parameter is not suitable,
we may want to issue a warning to the user and exit, as Example 11-5 shows.
Sub DisplayName(sName As String) If sName = "" then Msgbox "Please enter a name." Exit Sub End If MsgBox "Name entered is " & sName End Sub
While we are on the subject of exiting, we should comment on the
use of the End
statement, which will terminate a procedure. Simply
put, you should almost never
use the End
statement in VBA
programming, since it produces a rather abrupt termination of a
program. (I never like to say never.) Here is a partial list of what
happens when the End
statement is
executed:
Code execution stops abruptly, without invoking the Unload, QueryUnload, or Terminate event of any forms in the application, which means that forms are not given the opportunity to prevent the program from terminating or from performing any necessary cleanup.
All module-level variables and all static local variables are reset. (Nonstatic local variables go out of scope, as expected.) Objects created from class modules are destroyed.
Files opened using the Open
statement are closed.
While there may be some rather specialized situations in which
this behavior is desirable, you will no doubt recognize such a
situation if and when it arises. In the meantime, it is probably best
to simply avoid using the End
statement.
Just as variables and constants have a scope, so do procedures. We can declare a procedure using the
Public
or Private
keyword, as in:
Public Function AddOne(i As Integer) As Integer
or:
Private Function AddOne(i As Integer) As Integer
The difference is simple: a Private
procedure can only be called from
within the module in which it is defined, whereas a Public
procedure can be called from within
any module in the project.
Note that if the Public
or
Private
keyword is omitted from a
procedure declaration, then the procedure is considered to be Public
.
When we call a public procedure that lies in another code module, there is a potential problem with ambiguity, for there may be more than one public procedure with the same name in another module. VBA will execute the first one it finds, and this may not be the one we had in mind!
The solution is to use a qualified procedure name, which has the form:
ModuleName.ProcedureName
For instance, if a public procedure named AddOne
lies in a module named Utilities,
then we can call this procedure using the syntax:
Utilities.AddOne
3.144.151.126