In the preceding chapters, you've seen a lot of variables in the sample code, and you should understand the basic concept of a variable by now, but there are some topics that we have not explicitly discussed. These topics include rules for naming and declaring variables, as well as variable scope and lifetime, which we will cover here. If you are already an experienced programmer in another language, you can skim over much of this material, but there is some information that is particular to VBScript that you would do well to take in.
You might not be able to guess it based on the code examples we've presented so far, but declaring variables in VBScript is optional. That's right, you can just start using a new variable anywhere in your code without having declared it first. There is no absolute requirement that says that you must declare the variable first. As soon as VBScript encounters a new nondeclared variable in your code, it just allocates memory for it and keeps going. Here's an example (The script file for this code is OPTION_EXPL_NO_DECLARE.VBS; all code examples for this book are downloadable from the wrox.com Web site.).
lngFirst = 1 lngSecond = 2 lngThird = lngFirst + lngSecond MsgBox lngThird
Even though we did not explicitly declare any of the three variables, VBScript does not care. The code runs as you'd expect, and a dialog box comes up at the end displaying the number 3 . This sounds pretty convenient . This convenience comes at a very high price. Take a look at this code ( OPTION_EXPL_MISSPELLING.VBS ).
lngFirst = 1 lngSecond = 2 lngThird = lngFirst + lgnSecond MsgBox lngThird
Isn't this the same code as the previous example? Look again. Do you notice the misspelling in the third line? This is an easy mistake to make while you're typing in line after line of script code. The trouble is that this misspelling does not cause VBScript any trouble at all. It just thinks the misspelling is yet another new variable, so it allocates memory for it and gives it the initial subtype of Empty . When you ask VBScript to do math on an empty variable, it just treats the variable as a zero. So when this code runs, the dialog box displays the number 1 , rather than the number 3 we were expecting.
Easy enough to find the error and fix it in this simple do-nothing script, but what if this script contained dozens, or even hundreds, of lines of code? What if instead of adding 1 to 2 to get 3, we were adding 78523.6778262 to 2349.25385 and then dividing the result by 4.97432? Would you be able to notice a math error by looking at the result? If you were storing these numbers in variables, and you accidentally misspelled one of the variables in your code, you could end up with a math error that you might not notice for weeks-or worse yet, your boss or customer might find the error for you.
So what can we do to prevent this? The answer is a statement called Option Explicit . What you do is place the statement Option Explicit at the top of your script file, before any other statements appear. This tells VBScript that our code requires that all variables be explicitly declared before they can be used. Now VBScript will no longer let you introduce a new variable right in the middle of your code without declaring it first. Here's an example ( OPTION_EXPL_ERROR.VBS) .
Option Explicit Dim lngFirst Dim lngSecond Dim lngThird lngFirst = 1 lngSecond = 2 lngThird = lngFirst + lgnSecond MsgBox lngThird
Notice that we have added the Option Explicit statement to the top of our code. Since we have added Option Explicit , we must now declare all of our variables before we use them, which is what you see on the three lines following Option Explicit . Finally, notice that we have left our misspelling on the second-to-last line. We did this in order to illustrate what happens when you try to use an undeclared variable. If you try and run this code, VBScript will halt the execution with the following error: Variable is undefined: 'lgnSecond' . This is a good thing.
As long as we use Option Explicit , VBScript will catch our variable- related typing errors.
One thing that's very nice about Option Explicit is that it applies to the entire script file in which it resides. We have not discussed this too much so far in this book, but a single script file can contain multiple procedures, functions, and class definitions, and each class definition can itself contain multiple procedures and functions (we cover VBScript classes in Chapter 8). As long as you place Option Explicit at the top of the script file, all of the code within the file is covered.
Start a good habit today: every single time you start a new script file, before you do anything else, type the words Option Explicit at the top of the file. This will prevent silly typing errors from seriously messing up your code, and your fellow script developers (and customers) will appreciate it.
VBScript has a few rules for what names you can give to a variable. The rules are pretty simple, and leave you plenty of room to come up with clear, useful, understandable variable names .
Rule Number 1: VBScript variable names must begin with an alpha character.
An alpha character is any character between 'a' and 'z' (capital or lowercase). Non-alpha characters are pretty much everything else: numbers , punctuation marks, mathematical operators, and other special characters. For example, these are legal variable names:
And these are illegal variable names:
Rule Number 2: Numbers and the underscore (_) character can be used within the variable name , but all other non- alphanumeric characters are illegal.
VBScript does not like variable names that contain characters that are anything but numbers and letters . The lone exception to this is the underscore (_) character. (Some programmers find the underscore character to be useful for separating distinct words within a variable name (for example, customer_name ), while other programmers prefer to accomplish this by letting the mixed upper and lower case letters accomplish the same thing (for example, CustomerName ). For example, these are legal variable names:
And these are illegal variable names:
Rule Number 3: VBScript variable names cannot exceed 255 characters.
Hopefully, your variable names will not exceed 20 characters or so, but VBScript allows them to be as long as 255 characters.
These rules for variable naming should be pretty easy to follow, but it is important to make a distinction between coming up with variable names that are legal, and coming up with variable names that are clear, useful, and understandable. The fact that VBScript will allow you to use a variable name such as X99B2F012345 does not necessarily mean that it's a good idea to do so.
A variable name should make the purpose of the variable clear. If you're going to store the user 's name in a variable, a name like strUserName is a good one because it removes any doubt as to what the programmer intended the variable to be used for. Good variable names not only decrease the chances of errors creeping into your code, but also make the code itself easier for humans to read and understand.
Another principle that a large percentage of programmers have found useful is the 'Hungarian naming convention,' which we have mentioned a couple times before, and which we have been using throughout this and the preceding chapters. This convention simply involves using a prefix on the variable name to indicate what type of data the programmer intends for that variable to store.
For example, the variable name strUserName indicates not only that the variable should hold the user's name, but also that the subtype of the variable should be String . Similarly, the variable name lngFileCount indicates not only that the variable should hold a count of the number of files, but also that the subtype of the variable should be Long .
Appendix B of this book contains additional guidelines for naming variables, including a list of suggested data type prefixes.
At this point we will introduce the concept of procedures and functions, which are essential building blocks for more complex scripts. Procedures and functions allow you to modularize the code in your script into named blocks of code that perform specific functions. Modularization allows you to think about a more complex problem in a structured way, increases the readability and understandability of your code, and creates opportunities to reuse the same code multiple times within the same script.
Sometimes the word procedure is used in the generic sense to refer to either a procedure or a function, but we will do our best in this chapter to use the term procedure in the specific sense. A function is a named block of code that returns a value to the calling code, while a procedure is a named block of code that does not return a value to the calling code. Let's break down some of the new concepts in that last sentence .
lngCount = CLng("12")
A procedure is declared with the following syntax.
[PublicPrivate] Sub Name ([Argument1],[ArgumentN]) [code inside the procedure] End Sub
A named block of code that is a procedure is identified with Sub keyword. ('Sub' is short for 'subprocedure,' which is another way of saying 'procedure.') You can optionally precede the Sub keyword with the keywords of Public or Private , but these keywords are really relevant only within a class where you want some procedures to be visible outside the class and other procedures to be not visible (see Chapter 8).
In a normal script file (that is, one that is not a class or a Windows Script Component), the keywords Public and Private do not really do anything for you since no procedures, functions, or variables can be visible to any other scripts in other files. If you do not specify one or the other, Public is the default.
The ending boundary of the procedure must be defined with the keywords End Sub . Between the Sub and End Sub boundaries, normal VBScript syntax rules apply.
The rules for naming a procedure are the same as those for naming variables (see earlier). It is a good idea, however, to use clear, purposeful names that make it obvious what the purpose of the procedure is and what the code inside of it does. A good technique is to use verb-noun combinations such as ProcessOrder or GetName .
Arguments (also known as parameters ) are optional for a procedure, and you can use as many arguments as you would practically need (though a procedure with too many arguments is a sure sign of a poorly designed procedure that is doing too much; see under section Design Strategies for Procedures and Functions ). An argument is a value that you wish to 'pass into' the procedure so that the code inside the procedure will have access to it. The argument list must be surrounded by parentheses, and arguments should be separated by commas if a procedure has more than one argument. If a procedure does not have any arguments, the parentheses after the procedure name should be omitted.
Here is a bare-bones procedure that does not use any arguments ( PROCEDURE_SIMPLE.VBS ).
Option Explicit SayHelloToBill Sub SayHelloToBill MsgBox "Hello, Bill. Welcome to our script." End Sub
The first line in this example is not part of the procedure definition, but rather is the calling code that invokes the procedure. A procedure just sits there doing nothing unless there is some other code to call it.
Notice that we have omitted the Public / Private keywords and that there are no parentheses after the procedure name since it does not take any arguments. Also notice that the code inside of the procedure is indented; this is not required, but is a common convention since it makes the code easier to read. The indentation suggests the hierarchical relationship between the procedure and the code within it.
Here is a similar procedure that takes one argument (PROCEDURE_ARGUMENT.VBS ).
Option Explicit GreetUser "Bill" Sub GreetUser(strUserName) MsgBox "Hello, " & strUserName &_ ". Welcome to our script." End Sub
Notice how the addition of the strUserName argument, along with an adjustment to the procedure name, allows us to make the procedure more generic, which in turn makes it more reusable.
The syntax for a function is identical to that of a procedure, except that you change the keyword Sub to the keyword Function .
[PublicPrivate] Function Name ([Argument1],[ArgumentN]) [code inside the function] End Function
The rules for naming, Public / Private , and the declaration of arguments are the same as for procedures. As we've said, the distinction between a function and a procedure is that a function returns a value. Here is an example that illustrates the syntax for a function and how the code within a function sets the return value for the function ( FUNCTION_SIMPLE.VBS ).
Option Explicit Dim lngFirst Dim lngSecond lngFirst = 10 lngSecond = 20 MsgBox "The sum is: "& AddNumbers(lngFirst, lngSecond) Function AddNumbers(lngFirstNumber, lngSecondNumber) AddNumbers = lngFirstNumber + lngSecondNumber End Function
AddNumbers may not be the most useful function in the world, but it serves well to illustrate a few things. First, notice that this function has two arguments, lngFirstNumber and lngSecondNumber . The arguments are used inside of the function. Second, notice that the way the return value is specified is by referring to the name of the function within the code of the function. That's what is going on in this line.
AddNumbers = lngFirstNumber + lngSecondNumber
It's as if there is a nondeclared variable inside of the function that has the same exact name as the function itself. To set the return value of the function, you set the value of this invisible variable. You can do this from anywhere inside the function, and you can change the return value of the function repeatedly just as you can with a normal variable. If you set the return value more than once inside the function, the last such line of code to execute before exiting from the function is the one that sets the value.
Let's join together a procedure and a function in order to demonstrate how functions and procedures can be used together in a nested fashion ( PROCEDURE_FUNCTION_NESTED.VBS ).
Option Explicit GreetUser Sub GreetUser MsgBox "Hello, "& GetUserName & _ ". Welcome to our script." End Sub Function GetUserName GetUserName = InputBox("Please enter your name.") End Function
Notice how the GreetUser procedure calls the GetUserName function. Functions and procedures can work together in this way, which is how programs are built. Break your code up into specific modular building blocks of procedures and functions that do very specific things and then string the building blocks together in a logical manner.
This example brings up a good opportunity to introduce an important principle that we will discuss in more detail in the 'Design Strategies for Procedures and Functions' section of this chapter. There is a flaw in our nested procedure-function design. The GreetUser procedure has an unnecessary coupling to the GetUserName function. What this means is that GreetUser ' knows about' and depends on the GetUserName function. It depends on it because it makes a call to it; GreetUser won't know whom to greet if it does not ask GetUserName for a name.
Some amount of 'coupling' amongst code modules is necessary and good, but coupling is also something that you want to avoid if you don't need it. The more couplings in your program, the more complex it is. Some complexity is inevitable, but you want to reduce complexity as much as possible. When functions and procedures are coupled together in a haphazard manner, you get what is famously known as 'spaghetti code'-that is, code in which it is impossible to trace the logic because the logic twists and turns in a seemingly random pattern.
Here's a different version of the same script that eliminates the unnecessary coupling.
Option Explicit GreetUser GetUserName Sub GreetUser(strUserName) MsgBox "Hello, " & strUserName & _ ". Welcome to our script." End Sub Function GetUserName GetUserName = InputBox("Please enter your name.") End Function
The logic of the program is the same, but now we have decoupled GreetUser and GetUserName . We did this by restoring the strUserName argument to GreetUser and instead using the code at the top of the script to put the two functions together without either function 'knowing about' the other. Here is the interesting line of code in this script.
The return value from the GetUserName function is fed as the strUserName argument of the GreetUser function.
One final note about function syntax: programmers familiar with other languages may have noticed that there is no way to declare the data type of a function's return value. This makes sense if you remember that VBScript supports only one data type-the Variant . Since all variables are Variant s, there is no need for syntax that specifies the data type of a function.
One way that many VBScript programmers choose to help with code clarity in this regard is to use the same Hungarian type prefixes in front of their function names as they do for their variable names. For example, GetUserName could be renamed strGetUserName . However, if you choose to follow this convention, it is extra important to name your variables and functions so that they are easy to tell apart. Using the verb-noun convention for function names helps, such that it becomes obvious that strUserName is a variable and strGetUserName is a function.
In the preceding examples of procedures and functions, you may have noticed some differences in the syntax for calling a procedure as opposed to a function. There are indeed differences, and the VBScript compiler is very particular about them.
Here is one way to call a procedure.
Here is another.
These two conventions are functionally equivalent, and whichever you choose is largely a matter of taste. Some would argue that the second convention (using the Call keyword) is more clear, but both conventions are equally common and Visual Basic and VBScript programmers over time become very accustomed to one or the other.
The next example, however, is not legal for calling a procedure and will produce a compilation error.
Likewise, this example is also illegal for calling a procedure.
Call GreetUser "Bill"
When calling a procedure (as opposed to a function), if you choose not to use the Call keyword, then you cannot use parentheses around the argument value you are passing to the procedure. Conversely, if you do wish to use the Call keyword, then the parentheses are required.
The rules for calling functions are a bit different. If you want to receive the return value from a function, then you must not use the Call keyword and you must use parentheses around the argument list, like so:
lngSum = AddNumbers(10,20)
This syntax is illegal because it omits the parentheses.
lngSum = AddNumbers 10,20
And this is illegal as well because you cannot use the Call keyword when receiving the return value.
lngSum = Call AddNumbers(10,20)
You can, however, use the Call keyword if you do not wish to receive the return value of the function, but you have to use the parentheses.
You could also omit the Call keyword and still ignore the return value, but you must omit the parentheses in that case.
This begs the question: why would you ever want to call a function if you did not want the return value? The code in the preceding two examples might compile, but it looks awfully silly. Generally speaking, functions are functions because they return values and we call functions because we want the values they return.
However, there are cases where it makes sense to ignore the return value and call a function as if it were a procedure. The way we have been using MsgBox is a good example of this. MsgBox can be used as either a procedure or a function, depending on why you need it. MsgBox has dual purpose. It can just display a message for you, which is how we've been using it, or you can use it as a function to find out which button a user clicked on the dialog box. Here is a script that illustrates the two ways of using MsgBox ( MSGBOX_DUAL.VBS ).
Option Explicit Dim lngResponse Dim strUserName lngResponse = MsgBox("Would you like a greeting?", vbYesNo) If lngResponse = vbYes Then strUserName = GetUserName GreetUser strUserName End If Sub GreetUser(strUserName) MsgBox "Hello, " & strUserName & _ ". Welcome to our script." End Sub Function GetUserName GetUserName = InputBox("Please enter your name.") End Function
In this line of code we are using MsgBox as a function.
lngResponse = MsgBox("Would you like a greeting?", vbYesNo)
MsgBox has some optional arguments, one of which is the second argument that allows you to specify if you want the dialog box to offer more buttons than just the OK button. This use of the MsgBox function produces the dialog box shown in Figure 4-1.
If the user clicks the Yes button, the MsgBox function will return a certain value (defined as vbYes in this example). If the user clicked Yes , then the familiar GreetUser procedure will eventually be called, in which you can see how we can call MsgBox as a procedure instead of as a function.
vbYesNo and vbYes from the example are built-in VBScript 'named constants,' which are like variables with fixed, unchangeable values. (We will cover named constants later in this chapter.)
As we just saw with the MsgBox function in the previous section, procedures and functions can have optional arguments. If an argument is optional, then you don't have to pass anything to it. Generally, an optional argument will have a default value if you don't pass anything. Optional arguments will always appear at the end of the argument list; mandatory arguments must come first, followed by any optional arguments.
However, the procedures and functions you write yourself using VBScript cannot have optional arguments. The built-in VBScript procedures you call (such as MsgBox ) can have optional arguments, but your own VBScript procedures cannot. If you need to, you can get around this by defining mandatory arguments and interpreting a certain value (such as Null) to indicate that the caller wants that argument to be ignored. This kind of 'fake' optional argument can help you sometimes in a bind, but this technique is generally discouraged.
A procedure or function will exit naturally when the last line of code inside of it is done executing. However, sometimes you want to terminate a procedure sooner than that. In this case, you can use either of the statements Exit Sub (for procedures) or Exit Function (for functions). The code will stop executing wherever the Exit statement appears and the flow of the code will return to the caller.
With the simple functions we have been using as examples, there has not been an obvious place where you would want to use Exit Sub or Exit Function . Usually these statements are used inside of more complex code in situations where you have reached a logical stopping point or dead end in the logic. That said, many programmers discourage the use of these statements in favor of using a code structure that does not require them. Take this code, for example ( EXIT_SUB.VBS ).
Option Explicit GreetUser InputBox("Please enter your name.") Sub GreetUser(strUserName) If IsNumeric(strUserName) or IsDate(strUserName) Then MsgBox "That is not a legal name." Exit Sub End If MsgBox "Hello, "& strUserName & _ ". Welcome to our script." End Sub
Notice the Exit Sub in the GreetUser procedure. We have added some logic that tests to make sure the name is not a number or date, and if it is, we inform the user and use Exit Sub to terminate the procedure. However, many programmers would argue that there is a better way to do this that does not require the use of Exit Sub , as in this example ( EXIT_SUB_NOT_NEEDED.VBS ).
Option Explicit GreetUser InputBox("Please enter your name.") Sub GreetUser(strUserName) If IsNumeric(strUserName) or IsDate(strUserName) Then MsgBox "That is not a legal name." Else MsgBox "Hello, "& strUserName & _ ". Welcome to our script." End If End Sub
Notice that instead of using Exit Sub we have used an Else clause. The principle at work here is to design the procedure to have only one exit point, that being the implicit exit point at the end of the procedure. By definition, procedure or function with an Exit statement has more than one exit point, which some programmers would argue is poor design. The logic behind this principle is that procedures with multiple exit points are more prone to bugs and harder to understand.
The issue of variable scope and lifetime is closely tied concepts. A variable's scope is a boundary within which a variable is valid and accessible. The boundary within which a variable is declared is directly related to the lifetime of that variable. Script code that is executing outside of a variable's scope cannot access that variable. There are three types of scope that a VBScript variable can have:
There are three statements that you can use to declare variables: Dim , Private , and Public . (The ReDim statement that we introduced in the previous chapter also falls into this category of statements, but it is specifically used for the 'redimensioning' of already declared array variables.) These declaration statements are used in different situations, depending on the scope of the variable being declared:
We packed a lot of rules into these three points (and again, the examples we'll be getting to soon will make the rules clearer), so the following guidelines might make it easier to keep track of when to use Dim , Private , and Public .
Use Dim either at the procedure level to declare variables that are local to that procedure or at the script level. Dim is sort of the all-purpose keyword for declaring variables. Within non-class-based scripts and within scripts that will not be used as Windows Script Components, Private and Public don't have any effect different than that of Dim .
If you wish, you can use Private at the script level (instead of Dim ) to declare variables that will be available to the whole script. Use of Private becomes more important at the class level to declare variables that are available only within a class.
Use Public only to declare public properties for a class, but consider also the option of using a Private variable in combination with Property Let / Set and Get procedures. Even though Dim has the same effect as Public at the class level, it is more explicit, and therefore preferable, to not use Dim at the class level.
VBScript allows you to put more than one variable declaration on the same line. From a style standpoint, it is generally preferable to limit variable declarations to one per line, as our example scripts have, but this is not an absolute rule. For example, script programmers who are writing scripts that will be downloaded over the Web as part of an HTML file often prefer to put multiple declarations on a single line since it makes the file a little smaller. Sometimes, though, a programmer simply prefers to have more than one variable within a single declaration. This is one of those stylistic things on which programmers simply differ . It's nothing to get worked up about.
Here is an example of a valid multi-variable declaration.
Dim strUserName, strPassword, lngAge
And here is one using Private instead of Dim . The rules are the same whether you are using Dim , Private , or Public .
Private strUserName, strPassword, lngAge
Note, however, that you cannot mix declarations of differing scope on the same line. If you wanted to declare some Private and Public variables within a class, for instance, you would have to have two separate lines.
Private strUserName, strPassword Public lngAge, datBirthday, boolLikesPresents
Finally, VBScript does have limitations on the number of variables you can have within a script or procedure. You cannot have more than 127 procedure-level variables in any given procedure, and you cannot have any more than 127 script-level variables in any given script file. This should not cause you any trouble, however. If you are using this many variables within a script or procedure, you might want to rethink your design and break that giant procedure up into multiple procedures.
A variable's lifetime is closely tied to its scope. Lifetime, as the term suggests, refers to the time that a variable is in memory and available for use. A variable with procedure-level scope is only alive as long as that procedure is executing. A variable with script-level scope is alive as long as the script is running. A variable with class-level scope is alive only while some other code is using an object based on that class.
By limiting a variable's scope, you also limit its lifetime. Here is an important principle to keep in mind: you should limit a variable's lifetime, and therefore its scope, as much as you can. Since a variable takes up memory, and therefore operating system and script engine resources, you should keep it alive only as long as you need it.
By declaring a variable within the procedure in which it will be used, you keep the variable from taking up resources when the procedure in which it resides is not being executed. If you had a script file that contained ten procedures and functions, and you declared all of your variables at the script level, you would not only create some pretty confusing code, but also cause your script to take up more resources than necessary.
Really, though, resource consumption is not the most important reason for limiting variable scope. The most important reason is that limiting scope decreases the chance for programming errors and makes code more understandable and maintainable . If you have a script with several procedures and functions, and all of your variables are declared at the script level so that any of those procedures and functions can change the variables, then you've created a situation in which any code can be changing any variable at any time, and this can become very difficult for a programmer to keep up with.
It is best to design your scripts and classes so that you have the fewest possible number of variables that are available to all of the procedures and functions in your script or class. Instead, make use of local variables and procedure parameters as much as possible so that each procedure only has visibility to the data that it absolutely needs.
Let's look at an example that illustrates variable scope and lifetime in a non-class-based script ( SCOPE.VBS ).
Option Explicit Private datToday datToday = Date MsgBox "Tommorrow's date will be "& AddOneDay(datToday) & "." Function AddOneDay(datAny) Dim datResult datResult = DateAdd("d", 1, datAny) AddOneDay = datResult End Function
This script contains a function called AddOneDay() . The variable datResult is declared with Dim inside the function and has procedure-level scope, which means that it is not available to any of the code outside of the function. The variable datToday is declared with Private and has script-level scope. The variable datResult will be active only while the AddOneDay() function is executing, whereas the datToday variable will be active for the entire lifetime of the script.
Take another look at our last example ( SCOPE.VBS ). Note that we could have instead designed this script this way ( SCOPE_BAD_DESIGN.VBS ).
Option Explicit Private datToday datToday = Date AddOneDay MsgBox "Tommorrow's date will be " & datToday & "." Sub AddOneDay() datToday = DateAdd("d", 1, datToday) End Sub
This code is 100% legal and valid, and the ultimate result is the same as the original. Since datToday has script-level scope, it is available to the code inside of AddOneDay (which we've now changed from a function to a procedure), we simply designed AddOneDay to change datToday directly. It does work, but this kind of technique creates some problems.
First, we have lost the reusability of the AddOneDay function. Now AddOneDay is 'tightly coupled ' to the script-level variable datToday . If we want to copy AddOneDay and paste it into another script so we can reuse it, we've made our job a whole lot more difficult. When it was a stand-alone function with no knowledge of any data or code outside of itself, it was totally portable, generic, and reusable.
Second, while this simple situation in this simple script might seem innocent enough, try to imagine a more complex script with a couple dozen script-level variables. Imagine also a couple dozen procedures (no functions), and all of the procedures make changes to the script-level variables. Imagine these procedures calling each other and all of the script-level variables changing values rapidly . In imagining this script, hopefully you can also imagine that the logic of this script would be very chaotic and difficult to keep track of. There would be a lot of strange , hard to find bugs . Fixing one bug could easily create three others.
Keep in mind that we are not suggesting that you not use script-level variables or that you never change their values. It's all in how you go about doing it. The strategy you want to employ is to limit the number of places in your script that directly read and change script-level variables.
A great way to accomplish this is to think of the code at the top of your script file as the 'main' code, the code that controls the overall logic of the script. Lets call this code the puppet master. Then think of the procedures, functions, and classes inside of your script as puppets that have very specific jobs to do, but only do them when asked to by the puppet master code. Furthermore, the puppets are also kind of dumb. They don't know the big picture. The puppet master code keeps them in the dark by only giving them the information (by way of their arguments) they absolutely need in order to do their respective jobs. The puppet master forbids each puppet from changing any data outside of its very specific scope.
Some puppets are smarter than others, and the smarter puppets can enlist the help of the other puppets when necessary. In other words, sometimes one of the puppets might have a job that's somewhat complex, and it needs to call on one or more other puppets. At the lowest level you have very dumb puppets that do one very specific thing and don't get help from any other puppets.
Let's bring this back down to earth with an example, followed by some general principles. First, take a look at this script ( SENTENCE_NO_PROCS.VBS ).
Option Explicit Dim strSentence Dim strVerb Dim strNoun 'Start the sentence strSentence = "The " 'Get a noun from the user strNoun = InputBox("Please enter a noun (person, " & _ "place, or thing).") 'Add the noun to the sentence strSentence = strSentence & Trim(strNoun) & " " 'Get a verb from the user strVerb = InputBox("Please enter a past tense verb.") 'Add the verb to the sentence strSentence = strSentence & Trim(strVerb) 'Finish the sentence strSentence = strSentence & "." 'Display the sentence MsgBox strSentence
This essentially useless script goes through a series of steps to build a simple sentence based on input from the user. All of the code is in a single block with no procedures or functions, and the code shares access to script-level variables. Here is the same procedure broken into procedures and functions along the lines of our puppet master and puppets metaphor ( SENTENCE_WITH_PROCS.VBS ).
Option Explicit Dim strSentence strSentence = GetThe strSentence = strSentence & GetNoun & " " strSentence = strSentence & GetVerb strSentence = strSentence & GetPeriod DisplayMessage strSentence Function GetThe GetThe = "The " End Function Function GetNoun GetNoun = Trim(InputBox("Please enter a noun (person, place, or thing).")) End Function Function GetVerb GetVerb = Trim(InputBox("Please enter a past tense verb.")) End Function Function GetPeriod GetPeriod = "." End Function Sub DisplayMessage(strAny) MsgBox strAny End Sub
In this version we have a single script-level variable with a block of code at the top that coordinates the logic leading to the goal of the script: to build a sentence and display it to the user. The code at the top of the script uses a series of functions and one procedure to do the real work. Each function and procedure has a very specific job and makes no use of any script-level data. All of the functions and procedures are 'dumb' in that they do not have any 'knowledge' of the big picture. This makes them less error prone, easier to understand, and more reusable.
Another benefit is that you do not have to read the whole script in order to understand what's going on in this script. All you have to do is read these five lines and you have the entire big picture.
strSentence = GetThe strSentence = strSentence & GetNoun & " " strSentence = strSentence & GetVerb strSentence = strSentence & GetPeriod DisplayMessage strSentence
If, after getting the big picture, you want to dive into the specific details of how a particular step is accomplished, you know exactly where in the script to look. Even though this is a silly example not rooted in the real world, hopefully it illustrates the technique of strategically modularizing your scripts.
Here are some general principles to aid you in your script designs:
There is one concept we skipped while introducing arguments for procedures and functions: passing arguments by reference versus passing arguments by value . An argument is defined either by reference or by value depending on how it is declared in the procedure or function definition. A by reference argument is indicated with the ByRef keyword, whereas a by value argument can either be indicated with the ByVal keyword or by not specifying either ByRef or ByVal -that is, if you do not specify one or the other explicitly, ByVal is the default.
So what does all this mean exactly? You have probably noticed that when a variable is passed to a procedure or function as an argument that the code within the procedure can refer to that argument by name like any other variable. Specifying that an argument is by value means that the code within the procedure cannot make any permanent changes to the value of the variable. With by value, the code in the procedure can change the variable, but as soon as the procedure terminates, the changes to that variable/argument are discarded. On the other hand, with by reference, the changes are permanent and reflected in the calling code's copy of that variable.
Let's look at some examples. Here is a procedure with two arguments, one ByVal and one ByRef ( BYREF_BYVAL.VBS ).
Option Explicit Dim lngA Dim lngB lngA = 1 lngB = 1 ByRefByValExample lngA, lngB MsgBox "lngA = " & lngA & vbNewLine & _ "lngB = " & lngB Sub ByRefByValExample(ByRef lngFirst, ByVal lngSecond) lngFirst = lngFirst + 1 lngSecond = lngSecond + 1 End Sub
Running this code produces the dialog box shown in Figure 4-2.
First, notice that the lngA and lngB variables are declared at the script level, outside of the ByRefByValExample procedure and that both are initialized to a value of 1 . Second, notice that the lngFirst argument is declared as ByRef , and lngSecond as ByVal . Third, notice that both arguments are incremented by 1 inside of the procedure. Fourth, notice that in the dialog box only lngA (which was passed by reference) has a value of 2 after the procedure terminates.
Only lngA was changed because only lngA was passed by reference. Since lngB was passed by value, and changes made to it inside of the ByRefByValExample procedure are not reflected in the variable outside of the procedure.
Most of the time (we could even say almost all of the time), you will want to use ByVal for your procedure and function arguments. For many of the same reasons discussed in the previous sections about variable scope and lifetime, it is just plain safer and straightforward to use ByVal . There is nothing inherently wrong with ByRef , and there are sometimes good reasons to use it that are too involved to get into, but stick with ByVal until you run into a situation where you feel you need ByRef .
For example, here is a script that is using ByRef even though it does not have to ( BYREF.VBS ).
Option Explicit Dim strWord strWord = "aligator" AppendSuffix strWord MsgBox strWord Sub AppendSuffix(ByRef strAny) strAny = strAny & "XXX" End Sub
Here is a better example that eliminates the need for ByRef ( BYVAL.VBS ).
Option Explicit Dim strWord strWord = "aligator" strWord = AppendSuffix(strWord) MsgBox strWord Function AppendSuffix(ByVal strAny) AppendSuffix = strAny & "XXX" End Function
This example changes the procedure to a function such that the ByRef keyword is no longer needed. Note also that the ByVal keyword in this example is optional; leaving it out has the same effect since ByVal is the default.
In this section we will introduce a concept that has some controversy amongst programmers. When is it okay to use literals in your code, and when is it better to use a named constant instead? On one extreme, you have programmers who never use named constants in place of literals. On the other extreme, you have programmers who never use literals anywhere . In the middle, there is a balance that allows for some use of literals, but leans toward the use of named constants when doing so would increase clarity and reduce the likelihood of typing mistakes.
After reading the discussion given in the next section, you should have a good feel for where you stand on the literals and named constants controversy.
A literal is any piece of static data that appears in your code that is not stored in a variable or named constant. Literals can be strings of text, numbers , dates, or Boolean values. For example, the word "Hello" in the following code is a literal.
Dim strMessage strMessage = "Hello" MsgBox strMessage
The date 08/31/69 in the following code is also a literal.
Dim datBirthday datBirthday = #08/31/69# MsgBox "My birthday is " & datBirthday & "."
The string "My birthday is" is also a literal. Literals do not need to be stored in a variable to be considered a literal. And for one more example, the value True in the following code is also a literal.
Dim boolCanShowMsg boolCanShowMsg = True If boolCanShowMsg Then MsgBox "Hello there." End If
Many times, literals are just fine in your code, especially for simple scripts without a lot of code or complexity. Programmers use literals all the time. They are not inherently bad. However, there are many instances when the use of a named constant is preferable to using a literal.
A named constant is similar to a variable, except that its value cannot be changed at runtime. A variable is dynamic. While the code is running, any code within a variable's scope can change the value of it to something else. A named constant, on the other hand, is static. Once defined, it cannot be changed by any code during runtime-hence the name 'constant.'
You define a constant in your code using the Const statement. Here's an example ( NAMED_CONSTANT .VBS ).
Option Explicit Const GREETING = "Hello there, " Dim strUserName strUserName = InputBox("Please enter your name.") If Trim(strUserName) <> "" Then MsgBox GREETING & strUserName & "." End If
If the user types in the name ' William ', then this code results in the dialog box shown in Figure 4-3.
The Const statement defines the named constant called GREETING . The name of the constant is in all capital letters because this is the generally accepted convention for named constants. Defining constant names in all capital letters makes them easy to differentiate from variables , which are generally typed in either all lower case or mixed case. (Note, however, that VBScript is not case-sensitive. There is nothing in VBScript that enforces any capitalization standard. These are stylistic conventions only, adopted to make the code easier to read, understand, and maintain.) Additionally, since constants are usually written in all capital letters, distinct words within the constant's name are usually separated by the underscore (_) character, as in this example ( NAMED_CONSTANT2.VBS ).
Option Explicit Const RESPONSE_YES = "YES" Const RESPONSE_NO = "NO" Dim strResponse strResponse = InputBox("Is today a Tuesday? Please answer Yes or No.") strResponse = UCase(strResponse) If strResponse = RESPONSE_YES Then MsgBox "I love Tuesdays." ElseIf strResponse = RESPONSE_NO Then MsgBox "I will gladly pay you Tuesday for a hamburger today." Else MsgBox "Invalid response." End If
Constants also have scope, just like variables. While you cannot use the Dim statement to declare a constant, you can use Private and Public in front of the Const statement. However, these scope qualifications are optional. A constant declared at the script level automatically has script-level scope (meaning it is available to all procedures, functions, and classes within the script file.) A constant declared inside of procedure or function automatically has procedure-level scope (meaning that other code outside of the procedure cannot use the constant).
You can also declare multiple constants on one line, like so:
Const RESPONSE_YES = "YES", RESPONSE_NO = "No"
Finally, you cannot use variables or functions to set the value of a constant, because that would require the value to be set at runtime. The value of a constant must be defined at design time with a literal value, as in the aforementioned examples.
Some programmers will answer this question with 'always.' There is a school of thought that says that your code should never contain any literals. Other programmers never use named constants, either out of a lack of knowledge of their benefits, or out of just plain laziness . However, there is a reasonable middle ground. In a moment, we will look at some guidelines that might help us find this middle ground. However, first, let's examine some of the benefits that named constants can afford your code.
Named Constant Rule #1: If you are using a literal only once, it's probably okay to use it instead of creating a named constant.
Named Constant Rule #1 is especially true when you consider constants used in HTML-embedded script code, which must be downloaded over the Web. If you always used named constants in place of literals in client-side Web scripting, you could easily increase the size of the file that the user has to download to a point that is noticeable. And even in a server-side Web scripting scenario (where the script code is not downloaded to the user's browser), using constants everywhere can slow down the script execution. This is because the script engine has to process all the constants before it can execute the code that uses them.
However, if you are using the same literal over and over throughout the script, then replacing it with a named constant can really increase the readability of the code, and reduce mistakes from misspellings of the literal. A great technique in server-side Web ASP (Active Server Pages) scripting (see Chapter 14) is to put named constants in an 'include' file that can be reused in multiple scripts. Named constants are important, but sometimes you have to weigh the tradeoff .
Named Constant Rule #2: If using the constant in place of a literal makes the meaning of the code more clear, use the constant.
As we mentioned, Named Constant Rule #2 is especially true for literals that are numbers. If you are working with arrays with multiple dimensions, then using named constants in place of the array subscripts is a really good idea (see the next section of this chapter). If you are checking numeric codes that have different meanings based on the number, it's a great idea to use constants in place of the numbers, because the meaning of the numbers by themselves will probably not be clear. The same principle holds true of dates with a special meaning, or odd strings of characters whose meaning is not clear just from looking at them.
Many VBScript hosts , such as the Windows Script Host and Active Server Pages, support the use of constants that are built into VBScript. These are especially helpful for two reasons: first, it can be hard to remember a lot of the seemingly arbitrary numbers the many of the VBScript functions and procedures use as parameters and return values; and second, using these named constants makes your code a lot easier to read. We saw some examples of built-in named constants related to the VarType() and MsgBox() functions.
Appendix D of this book contains a list of many of the named constants that VBScript provides for you for free. You'll notice that many of these constants are easy to identify by the prefix vb . Also, you'll notice that these constants are usually written in mixed case, rather than all upper case. By way of example, lets take a look at some constants you can use in an optional parameter of the MsgBox() function (see Appendix A for details on the MsgBox() function).
Thus far, we have used the first parameter of MsgBox() multiple times throughout the book. This first parameter is the message that we want displayed in the dialog box. The MsgBox() function also takes several optional parameters, the second of which is the 'buttons' parameter, which lets you define different buttons and icons to appear on the dialog box. Here's an example.
MsgBox "The sky is falling!", 48
This code produces the dialog box shown in Figure 4-4.
By passing the number 48 as the second parameter of MsgBox() , we told it that we wanted the exclamation point icon to appear on the dialog box. Instead of using the not-so-clear number 48, we could have used the vbExclamation named constant instead.
MsgBox "The sky is falling!", vbExclamation
This code results in the exact same dialog box, but it's much more clear from reading the code what we're trying to do.
In this chapter we dove deeper into some of the details of VBScript variables. VBScript does not force you to declare variables before using them, but it is highly recommended that you include the Option Explicit statement at the top of all of your scripts so that VBScript will force variable declaration. Whether using Option Explicit or not,
VBScript has some rules for how you can name variables, including that variable names must start with a letter and cannot include most special characters .
In this chapter we also formally introduced procedures and functions, including the syntax for defining them and design principles on how to best make use of them. Once we introduced the concept of using procedures and functions to put boundaries around certain blocks of code, we explained how those boundaries define variable scope and lifetime.
We discussed the ByRef and ByVal keywords that can be used when declaring arguments for a procedure or function. We closed out the chapter by introducing named constants, which can, and often should, be used in your code in place of literal values.