Functions and procedures (or subroutines) are central to modern programming. Dividing our script into subroutines helps us to maintain and write programs by segregating related code into smaller, manageable sections. It also helps to reduce the number of lines of code we have to write by allowing us to reuse the same subroutine or function many times in different situations and from different parts of the program. In this section, we'll examine the different types of subroutines, how and why they are used, and how using subroutines helps to optimize code.
2.1.1 Defining Subroutines: The Sub . . . End Sub Construct
The Sub...End Sub construct is used to define a subroutine; that is, a procedure that performs some operation but does not return a value to its calling program. Blocks of code defined as subroutines with the Sub...End Sub construct can be called in three ways:
Some subroutines provide the means by which an object interfaces with the script. For instance, when a class defined with the Class...End Class construct is initialized, its Initialize event, if one has been defined, is executed automatically. For subroutines of this type, the routine's name can be constructed in only one way, as follows:
For example, Sub Class_Initialize is a valid name of a subroutine. This type of subroutine is known as an event handler or an event procedure.
Defining it as an event handler
A subroutine can be executed automatically if it is defined as an event handleras a routine that is executed whenever some event occurs. For the most part, the functionality to wire events and their event handlers is defined by the application environment rather than by the VBScript language itself. An exception, however, is the GetRef function, which allows you to define event handlers for Dynamic HTML pages in Internet Explorer.
Referring to it by name
A subroutine can be executed at any time by referring to it by name in another part of the script. (For additional details, including the syntax required to call subroutines, see Section 2.1.2" later in this chapter.) While it is possible to execute event procedures in this way, this method is most commonly used to execute custom subroutines. Custom subroutines are constructed to perform particular tasks within a program and can be assigned virtually any name that you like. They allow you to place code that's commonly used or that is shared by more than one part of a program in a single place, so that you don't have to duplicate the same code throughout your application.
Subroutine names follow the same rules as all identifiers, like classes, variables, and properties. This means that there are several very straightforward rules to remember when giving names to your subroutines:
For example, in the following:
Sub 123MySub( ) ' Illegal Sub My Sub Routine( ) ' Illegal
both names contain illegal subroutine names. However:
Sub MySub123( ) ' Legal Sub MySubRoutine( ) ' Legal
are legal subroutine names.
Most of these rules can be broken by enclosing the subroutine name in brackets. The following VBScript code for WSH, for instance, defines valid subroutines whose names begin with an underscore and a numeric string character, include an embedded space, and conflict with a VBScript reserved word:
[_Main] Public Sub [_Main] MsgBox "In_Main" [1Routine] [2 Routine] [Dim] End Sub Public Sub [1Routine] MsgBox "In 1Routine" EndSub Public Sub [2 Routine] MsgBox "In 2 Routine" End Sub Public Sub [Dim] MsgBox "In Dim" End Sub
Example 2-1 illustrates the use of a custom subroutine in a client-side script to contain code that is common to more than one part of an application. It provides a simple example of some common code that is placed in a custom subroutine. The web page in Example 2-1 contains three intrinsic HTML command buttons. But rather than handling the user's click of a particular button separately, each button's OnClick event procedure simply calls the ShowAlertBox routine. Had we not included the ShowAlertBox subroutine, which contains code common to all three event handlers in our web page, we would have had to create a script several times longer than the one shown in Example 2-1.
Along with showing how to use a custom subroutine to share code, Example 2-1 also demonstrates how to pass variables from one procedure to another, a topic discussed in greater depth in Section 2.1.4 later in this chapter. In particular, theShowAlertBox routine is passed the caption of the button on which the user has clicked so that it can display it in an alert box.
Example 2-1. Using a custom subroutine to share code
Sub cmdButton1_OnClick Call ShowAlertBox(cmdButton1.Value) End Sub Sub cmdButton2_OnClick ShowAlertBox cmdButton2.Value End Sub Sub cmdButton3_OnClick ShowAlertBox cmdButton3.Value End Sub Sub ShowAlertBox(strButtonValue) dim strMessage strMessage = "This is to let you know" & vbCrLf strMessage = strMessage & "you just pressed the button" & vbCrLf strMessage = strMessage & "marked " & strButtonValue Alert strMessage End Sub
2.1.2 Calling a Subroutine
In Example 2-1, you may have noticed that the cmdButton1_OnClick event procedure uses a different syntax to invoke the ShowAlertBox routine than the cmdButton2_OnClick and cmdButton3_OnClick procedures. The second form of the call to the ShowAlertBox function:
is currently the preferred method. Note that it is unclear that this is actually a call to a subroutine named ShowAlertBox. Presumably, ShowAlertBox could be a variable. In fact, in order to identify ShowAlertBox as a subroutine, we have to rely on a visual clue: it is followed by another variable on the same line of code. This assumes, of course, that the code is correct, and that we haven't inadvertently omitted an equal sign between two variables.
In contrast, invoking a procedure by using a Call statement like the following:
makes the code much more readable. You may prefer using it for this reason.
The rules for calling procedures are quite simple. If you use the Call statement, you must enclose the argument list in parentheses. If you do not use Call, you cannot use parentheses unless you're passing a single variable. In this case, though, parentheses also cause the variable to be passed by value rather than by reference to the subroutine (for the meanings of "by value" and "by reference," see Section 2.1.4 later in this chapter), a behavior that may have undesirable consequences.
2.1.3 Defining Functions: The Function . . . End Function Construct
As we've seen, subroutines created by the Sub...End Sub construct are used to manipulate data that is passed to them (assuming that the subroutine accepts parameters) or to perform some useful operation. However, subroutines have one major shortcoming: they don't return data, such as the results of their manipulations or information on whether they were able to execute successfully.
It is possible for asubroutine to "return" a value by passing it an argument by reference (a topic discussed in Section 2.1.4). However, that has one major disadvantage: it requires that you declare a variable to pass to the subroutine, even if you're not concerned with that variable's value or with the value "returned" by the subroutine.
There's also an additional way that a subroutine can return a value: you can pass the subroutine the value of a global variable that is visible throughout your routine. For instance, we could use the following code fragment to create a subroutine that cubes any value that is passed to it as a parameter:
Another routine can then access the result with a code fragment like the following:
<% Dim intVar intVar = 3 CubeIt intVar Response.Write cube %>
This approach, though, suffers from two limitations. First, it means that the global variable must remain in memory for the entire life of our script, even though the variable itself may be used only briefly, if at all. In most cases, this is a very minor concern, unless that variable is a large string or it's used on a particularly busy web server. Second, and much more important, it creates a variable that can be accessed and modified from anywhere within our script. This makes it very easy for a routine to accidentally modify the value of a variable that is used elsewhere in the script. The availability or unavailability of a variable within a particular procedure is called its scope. And in general, the variables in a well-designed application should have the most restrictive scope possible.
Through its support for functions, VBScript supports a much safer way of retrieving some value from a routine. Functions share many of the same characteristics as subroutines defined with the Sub...End Sub construct:
However, unlike subroutines, functions return some value to the calling procedure. This makes functions ideal for such uses as storing the code for frequently used calculations and conversions.
Functions are defined by using the Function...End Function construct, and by placing the function's code between these two statements. The full form of the Function...End Function statements is:
Function functionname(argumentlist) End Function
Defining a Function s Return Value
If you've used VB or VBA to create functions, you probably have used the As keyword to define the data type of the value returned by a function, as in the following statement:
Function CubeIt(ByVal x As Long) As Long
Since VBScript supports only the variant data type, though, the As keyword is not supported, and you don't have to worry about the data type returned by your custom function. All functions defined by the Function statement return data of type variant.
A function's argument list is defined in exactly the same way as a subroutine's: the list of arguments is separated by commas and is enclosed in parentheses.
So how do we have our function return a value to the calling procedure? Within the body of our function, we assign the value that we want our function to return to a variable whose name is the same as the name of the function, as illustrated by the following code fragment:
Function functionname(argumentlist) . . . some calculation or manipulation functionname = result of calculation or manipulation End Function
This variable is automatically initialized through the use of the Function statement. This means that if you're accustomed to defining your variables before using them, and especially if you've included the Option Explicit statement in your script, you should not use the Dim statement to explicitly initialize the variable for the function's return value.
To implement our earlier CubeIt procedure as a function rather than a subroutine, we dispense with the need to define a global variable to hold the cube of the argument passed to the function and enormously simplify our code, as the following code fragment shows:
<% Dim intVar intVar = 3 Response.Write CubeIt(intVar) %>
Once a custom function is correctly defined using the Function...End Function statement, it can be called just as if it were an intrinsic function that is built into the VBScript language. The function call itself can take either of two forms. The most common form involves using the function name and its argument list on the right side of an expression, and assigning its return value to a variable on the left side of the expression. For example, the most common way to call the CubeIt function is:
y = CubeIt(x)
This assigns the value returned by the CubeIt function to the variable y. Unlike a call to a subroutine, though, this means that the argument list, if one is present, must always be surrounded by parentheses. (If the function accepts no parameters, though, the opening and closing parentheses are typically still used, although they're not required.)
In some cases, you may not actually be concerned with a function's return value. This doesn't happen very oftenusually, you call afunction precisely in order to have it return some value, so ignoring its return value renders the function useless. Nevertheless, if you do want to discard a function's return value, you can call a function just like you would call a subroutine. For example:
Example 2-2 provides a real-world examplea client-side script that converts inches to either millimeters or metersthat shows how functions are defined and called. Along with two event procedures, it contains a function, sngMetric, that has a single argument, strInches, which is a string containing the number of inches that the user has input into the form's text box. The function converts this value to a single precision number, multiplies by 25.4, and, by storing it to the variable sngMetric, returns the result. The cmdButton1_OnClick and cmdButton2_OnClick event handlers call the function as necessary and pass the appropriate values to it. As you can see, the result returned by the sngMetric function is immediately displayed in a message box.
Example 2-2. Calling a function and returning a result
2.1.4 Passing Variables into a Subroutine
The ability to pass variables from one procedure to another is an important part of using custom procedures. It allows us to write custom "black box" routines that can behave differently depending on where the routine has been called from and also on the particular data values that the routine receives from the calling program.
The data is passed from a calling routine to a subroutine by an argument list. The argument list is delimited with commas and can contain any data types, including objects and arrays. For instance, the following mySubRoutine procedure expects three arguments: intDataIn1, strDataIn2, and lngDataIn3:
Sub AnotherSubRoutine( ) some code. . . . mySubRoutine intvar1, strvar2, lngvar3 more code that executes after mySubRoutine End Sub Sub mySubRoutine(intDataIn1, strDataIn2, lngDataIn3) code which uses incoming data End Sub
When mySubRoutine is called from AnotherSubRoutine, it is passed three variables as arguments: intvar1, strvar2, and lngvar3. So as you can see, the names of variables passed in the calling routine's argument list do not need to match the names in the custom procedure's argument list. However, the number of variables in the two argument lists does need to match or a runtime error results.
Passing Parameters by Reference
If you're accustomed to programming in VB or VBA, you'll recognize the way that you pass arguments in VBScript. However, in Versions 1 and 2 of VBScript, this wasn't the case. Parameters could be passed only by value, and there was no support for passingparameters by reference.
In addition, because VBScript is so flexible in its use of data types, you must take care when building subroutines that use data passed into them. The variables designated in the custom subroutine's argument list are automatically assigned the data types of the calling program's argument list. If a custom subroutine attempts to perform some inappropriate operation on the data passed to it, an error results, as the following code fragment illustrates:
Sub AnotherSubRoutine( ) some code. . . intVar1 = "Hello World" Call mySubRoutine (intvar1, strvar2, lngvar3) more code that executes after mySubRoutine End Sub Sub mySubRoutine(intDataIn1, strDataIn2, lngDataIn3) code that uses incoming data intResult = intDataIn1 * 10 'this will generate an error End Sub
The custom subroutine mySubRoutine assumed that intDataIn1 would be an integer, but instead the calling program passed it a string variable, intVar1. Therefore, VBScript automatically casts intDataIn1 as a string. The subroutine then produces a runtime error when it attempts to perform multiplication on a non-numeric variable. As you can see, while weakly typed languages like VBScript have many advantages, one of their major drawbacks is the fact that you must be on your guard for rogue data at all times.
You can pass an argument to a procedure either by reference or by value. By default, arguments are passed by reference, which means that the calling routine passes the called function or subroutine the actual variable (that is, its actual address in memory). As a result, any modifications made to the variable are reflected once control returns to the calling routine. The ASP code in Example 2-3 illustrates passing a variable by reference. The variable x is initially assigned a value of 10 in the DoSubroutine procedure. This value is then changed to 100 in the CallAnotherSub procedure. When control returns to the DoSubroutine procedure, the value of x remains 100 because the variable was passed by reference to CallAnotherSub.
Example 2-3. Passing a variable by reference
About to call DoSubroutine
<% DoSubroutine %>
The Sub statement for CallAnotherSub explicitly indicates that its single parameter, var1, is to be passedby reference because of the ByRef keyword. Since this is the default method of passing parameters, however, the keyword could have been omitted. The statement:
Sub CallAnotherSub(ByRef var1)
is identical to:
On the other hand, by value means that the calling routine passes the called function or subroutine a copy of the variable. This means that any changes to the variable's value are lost when control returns to the calling program. The ASP code in Example 2-4 illustrates passing a variable by value. As was also true in Example 2-3, the variable x is initially assigned a value of 10 in the DoSubroutine procedure. This value is then changed to 100 in the CallAnotherSub procedure. When control returns to the DoSubroutine procedure, the value of x remains 10 because the variable x was passed by value to CallAnotherSub.
Example 2-4. Passing a variable by value
About to call DoSubroutine
<% DoSubroutine %>
Note that the Sub statement for CallAnotherSub explicitly indicates that its single parameter, var1, is to be passed by value because of the ByVal keyword. This is necessary, since otherwise the variable would have been passed by reference.
To call a subroutine and pass it one or more arguments, you would use syntax like the following:
DoSomeSub x, y, z
where each argument in the argument list is separated from the other arguments by a comma, and the argument list is separated from the subroutine by a space. You cannot use parentheses to surround the argument list of a subroutine unless it has only a single argument.
To call a function, you can use the same syntax as you would use for a subroutine if you intend to discard the function's return value. For example:
DoSomeFunc x, y, z
passes three arguments to a function and ignores its return value. If the function has only a single argument, you can also call it and ignore its return value as follows:
More commonly, however, you are interested in the return value of a function. In that case, the argument list should be enclosed in parentheses, and each argument should be separated from other arguments by a comma. For example:
retval = DoSomeFunc(x, y, z)
Although the called routine defines whether an argument is to be passed to it by value or by reference, there is actually no way to force the caller to call a routine and pass it an argument by reference. This is because there is one additional way to pass an argument to a procedure that overrides the explicit or default ByRef keyword: you can enclose the argument in parentheses. This is a subtle difference that you should be aware of when passing parameters to procedures, since it can have unintended consequences. Imagine, for example, that we have the following subroutine, which accepts two arguments by reference:
Sub DoSomething(xl, x2)
The caller can pass the first argument to the subroutine by value by using the following syntax:
DoSomething (x1), x2
Similarly, the caller can pass the second argument to the subroutine by value by using the following syntax:
DoSomething x1, (x2)
If a subroutine has only a single parameter, then calling it with a syntax like the following:
also passes the argument x to it by value.
Overriding a by reference parameter when calling a function works similarly; arguments enclosed in parentheses are always passed by value rather than by reference. If the caller wishes to discard the function's return value, then a function is called exactly as if it were a subroutine, and by reference parameters are overridden in the same way as in calls to subroutines. If the caller retrieves the function's return value, then the function name must be followed by parentheses, as must the argument to be passed by value rather than by reference. For example, given a function with the signature:
Function CallFunction(var1, var2)
retVal = CallFunction(xl, (x2))
passes the x2 argument to the function by value rather than by reference. If a function has a single parameter, an argument can be passed to it by value rather than by reference using the following syntax:
retVal = CallFunction((x1))
Note the double parentheses around the single argument.
2.1.5 Exiting a Routine with the Exit Statement
Ordinarily, when you call a function or a subroutine, all code between the initial Function or Sub statement and the concluding End Function or End Sub statement is executed. In some cases, though, you may not want all of a routine's code to be executed.
For example, imagine a situation in which you only want to execute a subroutine if a particular condition is met. One way of implementing this in your code is to test for the condition before calling the subroutine, as follows:
. . . some code If condition Then Call MySubRoutine( ) End if . . . more code
However, if you call the routine from multiple locations in your code, and you want to apply this test to each call, you'll have to include this control structure at every place in the script in which you call the subroutine. To avoid this redundant code, it's better to call the subroutine regardless of the condition, and to place the test within the subroutine. One way of doing this is as follows:
Sub MySubRoutine( ) If condition then . . . all our subroutine code End if End Sub
This is all well and good, and quite legal. However, in a large and complex subroutine, the End If statement becomes visually lost, especially if there are several conditions to be met. The preferred alternative is the Exit Sub and the Exit Function statements, which are used with the Sub . . . End Sub and Function . . . End Function constructs, respectively. Our conditional test at the beginning of a subroutine then appears as follows if we use the Exit Sub statement:
Sub MySubRoutine( ) If Not condition Then Exit Sub . . . all our subroutine code End Sub
Exit Sub and Exit Function immediately pass execution of the program back to the calling procedure; the code after the Exit statement is never executed. As you can see from the previous code fragment, the code is clean and clearly understandable. If the particular condition is not met, the remainder of the subroutine is not executed. Like the Exit Do and Exit For statements, any number of Exit Sub or Exit Function statements can be placed anywhere within a procedure, as the following code fragment demonstrates:
Function functionname(argumentlist) . . . some calculation or manipulation If condition1 Then functionname = result of calculation or manipulation Exit Function End If . . . perhaps some more code If condition2 Then functionname = result of calculation or manipulation Exit Function End If End Function