Simply stated, a subroutine is a set of instructions designed to accomplish one specific task. For example, you might write a subroutine named CheckRange() that checks certain numeric values to make sure they fall within a certain range. You might write another subroutine named CalcSalesTax() that calculates the sales tax for an order. In each case, the purpose of the subroutine is to accomplish one narrowly defined task.
Why Use Subroutines?
One of the primary reasons to use subroutines is to eliminate duplicate code. For example, if a program asks the user to enter a dozen numeric values into the program, you could write a CheckRange() subroutine once and then use it to check all 12 numbers . Which is better: writing a dozen almost identical sections of code to check each number or writing a subroutine once and being done with it? Using subroutines eliminates the need for duplicate code in many programs.
A second reason for using subroutines results from the first: Subroutines simplify programs. Because subroutines reduce the amount of code in a program, there is less code to read and understand. This also means there is less code to debug and maintain. These are good things.
A third reason for using subroutines is that doing so promotes modularity. That is, when you write a subroutine, you compartmentalize the code. When you write a subroutine, you give it some kind of descriptive name , such as CalcSalesTax() . Next year, when your state is going broke and decides to raise the sales tax rate, you know exactly where to go in your code to make the necessary change. You don't have to go searching through all your code, looking for all the places where the sales taxes are figured. With a subroutine, you need to make one program change and ”bingo! ”you're done.
A fourth reason for using subroutines derives from the third reason. Because subroutines promote modular code, they help you more easily reuse code. For example, if you write a new program that deals with sales taxes, you can steal the CalcSalesTax() code from your existing program and use it in the new one. One reason stealing something is so popular is because it's much easier than working for it. The same is true in programming. Reusing fully functional and debugged code is easier than writing new code.
Writing a Simple Subroutine
Suppose you have a program that saves a person's name and phone number to a disk data file. Further assume that there is a Save button that saves the data to the file. Also assume that the Save button is also responsible for clearing out the text boxes after the data is saved. This simple program might use a form that looks like the one shown in Figure 5.1.
Figure 5.1. A program form for saving a name and phone number.
What you want to do is write a simple subroutine that clears out the txtName and txtPhone text boxes when the user clicks the btnSave button. The following is the general syntax structure for a subroutine:
Private Sub SubName () ` Code statements that are the body of this subroutine End Sub
The first line of a subroutine begins with the word Private . The word Private is an access specifier . We don't discuss access specifiers until Chapter 7, "Arrays," so for now you can think of Private as meaning that this subroutine can be used only by code in the currently active form. In other words, if the program had multiple forms, this subroutine would only pertain to, or be private to, the form shown in Figure 5.1.
The second word in the first line is Sub . As you have probably guessed, this is an abbreviation for the word subroutine. Sub tells Visual Basic .NET that you are in the process of defining a subroutine.
The third word in the first line, SubName () , is the name of the subroutine that you are defining. In naming subroutines, you must follow the same rules that you follow for naming variables . You should make the name of the subroutine descriptive so that it gives you (and anyone else who might read the code) a good idea of what the subroutine does. In your simple program, you might choose to name the subroutine ClearTextboxes() because it describes what you want the subroutine to do.
The last program statement is End Sub . This marks the end of the subroutine. Everything between the first and last lines forms the code that is called the body of the subroutine. The body of the subroutine contains all the program statements that are necessary for the subroutine to accomplish its task.
In the simple program you're writing, you simply want the subroutine to clear out the text boxes after the information is saved to disk. The actual code for the subroutine is very simple, as shown in Listing 5.1.
Listing 5.1 Code for the ClearTextboxes() Subroutine
Private Sub ClearTextboxes() txtName.Text = "" txtPhone.Text = "" End Sub
All the subroutine does is set the txtName and txtPhone text boxes to contain empty strings.
Calling a Subroutine
Now that you have written a subroutine, how do you use it? When you want to execute the code associated with a subroutine, you call the subroutine. To call a subroutine, all you have to do is write the name of the subroutine as a program statement at that point in the program where you want the subroutine to be executed. In your sample program, you would probably place the call to the subroutine in the btnSave object's Click() event, as shown in Listing 5.2.
Listing 5.2 Code for the btnSave_Click() Event Subroutine
Private Sub btnSave_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles btnSave.Click SaveNameAndPhoneNumber() ClearTextboxes() End Sub
In the code in Listing 5.2, it appears that there are two subroutines. The first subroutine that is called is named SaveNameAndPhoneNumber() . As you might guess, the purpose of calling this subroutine would be to save the name and phone number that was just entered by the user to a disk data file. After that subroutine finishes its task, the ClearTextboxes() subroutine is called.
There are several things to notice in this simple piece of code. First, the btnSave object's Click() event is itself a subroutine. Indeed, all button Click() events (plus many others that are examined in later chapters) are subroutines that have their skeleton code written automatically by Visual Basic .NET whenever they are added to forms.
What's skeleton code? Perhaps you've noticed that whenever you add a button object to a form, Visual Basic .NET automatically writes this subroutine "header code":
Private Sub btnSave_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles btnSave.Click
Visual Basic .NET also automatically supplies this subroutine "tail code":
All you, as the programmer, have to write is the body of the subroutine. In the subroutine example in Listing 5.2, the body code is simply the two subroutine calls.
You should notice that in Listing 5.1, the ClearTextboxes() subroutine is immediately followed by a set of empty parentheses. However, in the btnSave_Click() subroutine in Listing 5.2, the parentheses are not empty.
Sometimes, subroutines need additional data in order to perform their specific task. This needed information can be passed to the subroutine as a subroutine parameter. If a subroutine parameter is needed, the name of the subroutine parameter appears between the two parentheses.
If a subroutine needs a subroutine parameter, you must supply the name of the parameter, along with its data type. For example, in Listing 5.2, you can see that the btnSave_Click() subroutine has two subroutine parameters, named sender and e . At this point you don't need to completely understand the details of everything you see between the parentheses; the important thing to notice is that two subroutine parameters are being passed to the subroutine, and they are separated by a comma.
How do you know whether to use subroutine parameters? Alas, there is no hard-and-fast rule. If the subroutine needs to know about some information that is hidden away in some other part of the program, you probably need to pass that data to the subroutine as a parameter. You are free to use zero or as many parameters as you need. All you need do is separate the parameters with commas.
A Sample Program with Subroutine Parameters
To put your knowledge of subroutines to use, you can now write a simple program that calculates the sales tax due on a sale of an item. To begin, create a new project named SalesTax and supply it with the objects shown in Figure 5.2. The two text boxes are called txtCost and txtTax , and the two buttons are the names we normally use: btnCalc and btnExit .
Figure 5.2. The objects for the SalesTax project.
Your job is to write a subroutine that calculates the sales tax due on the sale of a particular item. You can use the subroutine name presented earlier in the chapter: CalcSalesTax() .
The Parameters to Use
You now need to think about the design of the CalcSalesTax() subroutine. A reasonable starting place is to determine what information the subroutine needs to perform its task. In order to calculate the amount of sales tax to collect, the subroutine needs to know the cost of the item being purchased. Also, the subroutine needs to know the sales tax rate. Finally, the subroutine needs to return to the caller a numeric value that is the amount of the sales tax to be collected. (The caller is the name of the routine that called the subroutine. You'll learn more on this in a moment.)
You can summarize the data needs for the CalcSalesTax() subroutine as follows :
Now that you know what information the subroutine needs, you need to consider the form this information should take.
In the SalesTax program, the cost of the item could vary substantially each time the program is run. The sales tax due will also vary because it is dependent on the item's cost. In the short run, however, the sales tax rate will probably not vary. Data that will likely not vary in the short run are usually not candidates for subroutine parameters. Still, the subroutine needs to have the sales tax rate in order to perform its calculation. So how should we handle this design issue?
You could make the sales tax rate a symbolic constant. A symbolic constant is a data item that is assigned a constant value. You could use the following line of code to define a symbolic constant for use in the SalesTax program:
Const SalesTaxRate As Double = 0.05
This line of code defines a symbolic constant named SalesTaxRate as a Double data type. The Const keyword tells Visual Basic .NET that this data item is a constant and its value will not change during the life of the program. The name of the symbolic constant ( SalesTaxRate ) follows the Const keyword, which is then followed by the data type being defined (that is, As Double ). The last part of the definition simply assigns the numeric value for the symbolic constant. Note that you must perform this assignment as part of the definition. (This makes sense. After all, what's the purpose of a constant if you can reassign it later in the program?) When the value is assigned to the symbolic constant, its value is etched in stone, and any attempt to change it generates an error message.
This definition should be placed after the Inherits statement in the program. You can see the Const statement near the top of the Form window in Figure 5.3.
Figure 5.3. The definition of a symbolic constant.
Why use a symbolic constant? Why not just use a variable and assign it a value? Well, you could do that, and the program would work just fine in this particular program. However, using a symbolic constant offers certain advantages.
First, if you make SalesTaxRate a constant, there is no way that its value is going to be changed unintentionally by any other part of the program. This is simply a defensive method of coding to make sure that something that could never happen (for example, somebody being stupid enough to intentionally change the sales tax rate) doesn't. This is especially a good idea when a team of programmers develops a program.
Second, because SalesTaxRate is a constant, its definition is made at a known place in the program. Convention places such definitions near the top of the program code, after the Inherits keyword. Several years from now, when the tax rate changes, the SalesTaxRate constant will make it easy to locate the data item.
A third reason to use a symbolic constant relates to the second reason. Suppose your best friend wants you to write a program, but your friend is colorblind. Therefore, you decide that your program is going to manage the foreground and background color of every piece of text your friend is ever going to read in the program. Your friend can see white text on a blue background.
If the program is fairly complex, there could be hundreds of places where you need to change the foreground and background colors of the text box. This means there could be hundreds of lines of code, in dozens of forms, that look similar to this:
txtFirstName.ForeColor = &H00FFFFFF& ' This is white txtFirstName.BackColor = &H00FF0000 ' This is blue
The notation used for the values shown here is for hexadecimal constants; this is a common way to state color values. For the moment you need not concern yourself with how these values are derived.
Finally, you finish the program, and your friend loves it. As he is leaving your office, you open the door a little too quickly and bang his head pretty hard with the door. After he quits seeing stars, he looks at the computer and notices that he can no longer read white text on a blue background. After a little experimentation, you discover that he can now read green letters on a red background.
So you begin the process of performing hundreds of search-and-replace changes throughout every aspect of the program code. This is a very error-prone process. Surely there is a better way.
If you had defined the following two symbolic constants:
Const ForegroundText as Long = &H0000FF00& ' This is green Const BackgroundText as Long = &H000000FF&RGB(0, 0, 255) ' This is red
and used them throughout the program as follows:
txtFirstName.ForeColor = ForegroundText txtFirstName.BackColor = BackgroundText
All you'd have to do to accommodate your friend's new color scheme is change the color values in the definitions of the two symbolic constants and recompile the program. In a matter of seconds, Visual Basic .NET could change the hundreds of assignment statements for the foreground and background colors to use the new color values.
Symbolic constants are perfect for values that will not change in the short run but might change at some future date. Also, symbolic constants can often make code more understandable. For example, which of the following statements gives you a better idea of what the program is doing at this point ”this:
due = p * .05
due = p * SalesTaxRate
Selecting meaningful names for symbolic constants (or any variable, for that matter) helps document what the code is doing.
Now you know that there is no reason to make the sales tax rate an argument to the subroutine. You can simply define the sales tax rate as a symbolic constant named SalesTaxRate , as shown in Figure 5.3. Now you can press on to writing the subroutine.
The CalcSalesTax() Subroutine
You now know that you need two parameters passed to the subroutine: the price of the item and the sales tax to be collected. Therefore, you might write your code as shown in Listing 5.3.
Listing 5.3 Code for the CalcSalesTax() Subroutine
Private Sub CalcSalesTax(ByVal Price As Double, ByVal SalesTax As Double) ' Purpose: This subroutine calculates the sales tax ' that is to be collected for a given item. ' ' Arguments: ' Price the purchase price of the item ' SalesTax the sales tax amount to collect ' ' Caution: the calculation uses the symbolic constant ' SalesTaxRate, which holds the current sales ' tax rate. SalesTax = Price * SalesTaxRate End Sub
Notice in Listing 5.3 that program comments are used to state the purpose of the subroutine and the parameters it uses. A cautionary statement appears at the end of the comments to tell the reader what the SalesTaxRate is. After the comment is the actual body of code for the subroutine. This subroutine is very simple and only needs a single line of code.
Calling a Subroutine
When the code for your subroutine is finished, you need to add the following lines to the btnSave object's Click() event:
Dim ItemPrice As Double, SalesTaxDue As Double ItemPrice = CDbl(txtCost.Text) CalcSalesTax(ItemPrice, SalesTaxDue) ' Call the subroutine txtTax.Text = CStr(SalesTaxDue)
The first line defines two Double data types for use in the program. The second line simply converts the cost the user types into the program into a Double and assigns it to the ItemPrice variable. The third line is the actual call to the subroutine. Notice that the first parameter in the parentheses is ItemPrice and the second parameter is the SalesTaxDue variable. This sequence is important, and it must match the parameter sequence for the CalcSalesTax() subroutine. Imagine how things would get messed up if you reversed their order.
Now compile and run the program. A sample run is shown in Figure 5.4.
Figure 5.4. A sample run of the SalesTax program.
Wait a minute. If the item price is $100 and the sales tax rate is .05, the txtTax text box should be displaying 5, not 0. Something's not right. The following sections explain the problem and how you fix it.
The ByVal and ByRef Keywords
If you type the following statement line into the SalesTax program:
Private Sub CalcSalesTax(Price As Double, SalesTax As Double)
and press the Enter key, Visual Basic .NET immediately changes the program line to this:
Private Sub CalcSalesTax(ByVal Price As Double, ByVal SalesTax As Double)
Notice that Visual Basic .NET immediately prefixes both parameter names with the keyword ByVal . The ByVal keyword is the default method for passing a parameter to a subroutine. The impact that ByVal has on parameters is extremely important for you to understand.
Simply stated, ByVal means that a copy of the parameter's current value is passed to the subroutine. In terms of the discussion in Chapter 4, it is the rvalue of the parameter that is passed to the subroutine. In Figure 5.4, you can see that the user typed in the value 100 , so that this statement in the btnSave object's Click() event:
ItemPrice = CDbl(txtCost.Text)
finds the variable ItemPrice equal to 100 . Now, let's see what the next statement does:
CalcSalesTax(ItemPrice, SalesTaxDue) ' Call the subroutine
Because Visual Basic .NET says that the parameters in the CalcSalesTax() subroutine are ByVal , when Visual Basic .NET sees the call to CalcSalesTax() , it creates copies of the current values for ItemPrice and SalesTaxDue and sends those copies to CalcSalesTax() . How does Visual Basic .NET send the copies of ItemPrice and SalesTaxDue to the subroutine? It does it via a mechanism called the stack, described in the following section.
The stack is a chunk of memory that is used to hold temporary values while a program executes. What's interesting about the stack is that it works like a last-in, first-out (LIFO) buffer.
To envision how a stack works, think of the stack of salad plates at a buffet line. The busboy comes out and starts putting plates on the stack. Each new plate pushes the previous plate deeper into the stack. When the busboy is finished, the last plate he put on the stack is the first one a customer uses. That is, the last plate in the stack is the first plate off the stack.
A computer stack works much the same way as the stack of salad plates. When the CalcSalesTax() subroutine is called, the first thing Visual Basic .NET does is push the memory address of the next program instruction on the stack. For example, if the call to CalcSalesTax() is located at memory address 50,000, Visual Basic .NET pushes the next memory address, 50,001, on the stack. Why does it do this? When it does this, the last thing to come off the stack when the subroutine is done doing its job is the memory address of where the program should resume execution. This memory address takes 4 bytes of stack memory.
In the btnSave object's Click() event, the two variables of interest are ItemPrice and SalesTaxDue . Assume that ItemPrice is stored at memory address 80,000 and SalesTaxDue is stored at 80,100. Figure 5.5 shows how their lvalue and rvalue values look just before you call CalcSalesTax() .
Figure 5.5. lvalue and rvalue values for ItemPrice and SalesTaxDue .
The next thing Visual Basic .NET does is copy the current value of Price onto the stack. Because Price is a Double , Visual Basic .NET makes a copy of Price 's rvalue ( 100 ), forms it into a Double , and shoves that 8-byte copy onto the stack. Finally, it takes the current rvalue of SalesTaxDue , which is by default, makes it into an 8-byte Double , and shoves it onto the stack. Figure 5.6 shows what the stack looks like the instant before program control is sent to CalcSalesTax() .
Figure 5.6. A picture of the stack when CalcSalesTax() is called.
Now let's look at the code for CalcSalesTax() and see what Visual Basic .NET does there. Because Visual Basic .NET has made the parameters ByVal variables by default, Visual Basic .NET knows that the stack has copies of data on the stack. Therefore, Visual Basic .NET creates two temporary variables named SalesTax and Price , each of type Double . How does Visual Basic .NET know that these two variables are on the stack? It knows because the first line of the subroutine says there are two Double data types being passed to the CalcSalesTax() subroutine.
Assume that Visual Basic .NET places the two temporary variables at the lvalue values shown in Figure 5.7.
Figure 5.7. Temporary stack variables created for CalcSalesTax() 's parameters.
Figure 5.7 shows the rvalue value s, with question marks because Visual Basic .NET knows that the "real" values for these temporary variables are on the stack, so there's no reason to initialize their rvalue value s to . That would be a waste of time. Therefore, the actual rvalue values for Price and SalesTax at the moment they are created are whatever random bit patterns happen to exist at memory locations 5,000 and 6,000.
As mentioned earlier, Visual Basic .NET knows that there are two Double values on the stack. It also knows that the last parameter in the parameter list will be the first one to be popped off the stack because the stack is a LIFO buffer. Therefore, the first thing Visual Basic .NET does is pop off the top value (all 8 bytes of it) from the stack and shove it into SalesTax . The stack now looks as shown in Figure 5.8.
Figure 5.8. The stack after SalesTaxDue 's rvalue is popped off.
Because the value of SalesTax has been popped off the stack, there are only two data items left on the stack, as shown in Figure 5.8. Likewise, Figure 5.7 has now changed because you've taken off the second parameter and made it the rvalue of SalesTax . This is shown in Figure 5.9.
Figure 5.9. lvalue and rvalue values for Price and SalesTax after the first parameter is popped off the stack.
Notice that the only difference between Figures 5.7 and 5.9 is the zero value that was popped off the stack and into the rvalue for SalesTax .
The next thing Visual Basic .NET does is pop the rvalue for Price off the stack. The stack picture now looks as shown in Figure 5.10.
Figure 5.10. The stack after Price 's rvalue is popped off.
Now the only data left on the stack is the memory address where program execution resumes when CalcSalesTax() is finished. Visual Basic .NET has popped the first parameter value off the stack and assigned it to Price . The final values for the temporary variables are shown in Figure 5.11.
Figure 5.11. lvalue and rvalue values for Price and SalesTax after both parameters are popped off the stack.
Visual Basic .NET now has the parameter values set up properly, and you can execute the next program statement in the subroutine. This is the program line:
SalesTax = Price * SalesTaxRate
Using the rvalue values in Figure 5.10, that line becomes this:
SalesTax = 100 * .05 SalesTax = 5.0
As a result, the rvalue for SalesTax becomes 5.0 , as shown in Figure 5.12.
Figure 5.12. lvalue and rvalue values for Price and SalesTax after Price is multiplied by SalesTaxRate .
Here is where you went wrong: The instant Visual Basic .NET sees the End Sub statement in Listing 5.3, it discards the temporary variables Price and SalesTax ”and all the data associated with them. Good grief ! You do all that work and then just toss the results away when the End Sub is executed. In fact, when Visual Basic .NET sees the End Sub , it releases the memory for Price and SalesTax , and then it pops off the last value on the stack. Visual Basic .NET knows that this last value is the memory address where it should resume program execution, so Visual Basic .NET jumps to memory address 50,001 and starts executing the instructions stored at that address. Price and SalesTax are gone completely, as are their lvalue and rvalue values.
Back in the btnSave object's Click() event, ItemPrice and SalesTaxDue are the same as they were before. After all, their lvalue values were unknown to the CalcSalesTax() subroutine. How can you expect CalcSalesTax() to change SalesTaxDue if the subroutine doesn't know the lvalue where SalesTaxDue is in memory? There is no way anything can change SalesTaxDue if it doesn't know the lvalue . Think about it. Given that CalcSalesTax() doesn't know the lvalue of SalesTaxDue , how can you fix the problem?
Fixing the Problem
If the CalcSalesTax() subroutine had the lvalue of SalesTaxDue , it could change its rvalue . The fix is simple. You take the first program line for the CalcSalesTax() subroutine and change it so it looks like this:
Private Sub CalcSalesTax(ByVal Price As Double, ByRef SalesTax As Double)
The only change to this line is that you change the ByVal keyword for SalesTax to ByRef . The ByRef keyword means that the parameter is being passed "by reference" to the subroutine. In our words, instead of sending a ByVal copy of SalesTaxDue to the subroutine, you are sending its ByRef lvalue instead. Because an lvalue is being sent, Visual Basic .NET does not create a temporary variable for SalesTax . Instead, CalcSalesTax() uses the lvalue passed to it for anything that might involve SalesTax . In other words, CalcSalesTax() is really using the SalesTaxDue variable that was defined in the btnSave object's Click() event.
This one-word change makes a huge difference in the way things work in the subroutine. First of all, the stack picture in Figure 5.6 now becomes that shown in Figure 5.13.
Figure 5.13. A new picture of the stack when CalcSalesTax() is called.
Note that you are now sending the lvalue of SaleTaxDue ” not the rvalue ”to the subroutine. Obviously, this means that CalcSalesTax() now knows where to find the SaleTaxDue variable in memory.
Because of the change to the first line of CalcSalesTax() , Visual Basic .NET also knows that the first parameter off the stack is not an rvalue , but rather an lvalue . Therefore, Figure 5.10 now changes to Figure 5.14.
Figure 5.14. lvalue and rvalue values for Price and SalesTax after you make SalesTax a ByRef parameter.
Notice that the lvalue for SalesTax in CalcSalesTax() is the same as SalesTaxDue in the btnSave object's Click() event. Now, when the subroutine multiplies Price by SalesTaxRate in the program line:
SalesTax = Price * SalesTaxRate
Visual Basic .NET takes the result, 5.0 , and places that value as a Double into memory address 80,100, as shown in Figure 5.14. It then sees the End Sub , cleans up the temporary variable named Price that it created, and resumes execution at memory address 50,001. There is no cleanup for SalesTax . Visual Basic .NET knows not to create a temporary variable because an lvalue , not an rvalue , was sent for SalesTax .
You should now change the first line in the CalcSalesTax() subroutine to use the ByRef keyword and recompile the program. As you can see in Figure 5.15, all is now right with the world.
Figure 5.15. A sample program run after you use the ByRef keyword for the SalesTax parameter.
Define Versus Declare
Throughout this book I have been very precise in my use of the term define. When I have said something like "The Dim statement defines a variable named ItemPrice ," I have had a very precise meaning. Specifically, the term define means that storage for the object is returned by Windows, and a new lvalue is now available for that object in the symbol table.
Programmers often use the terms define and declare as if they are interchangeable. They are not. When you define an object, storage is allocated for the object, and an lvalue is determined and written into the symbol table. When you declare an object, you are simply filling in attributes about the object (for example, the data type, the name), but no storage is requested and no lvalue is returned.
In the previous section, the second parameter in the CalcSalesTax() subroutine is named SalesTax . However, because it is a ByRef parameter, its storage and, hence, its lvalue , are determined before program control ever reaches the CalcSalesTax() subroutine. Therefore, any ByRef parameter references are data declarations, not data definitions. Visual Basic .NET uses data declarations so that it knows how to process the object, not where to create it. However, ByRef objects already exist and have lvalue values because they must have been previously defined elsewhere in the program.
I admit that this is some pretty hairy stuff. However, it is critical that you understand the difference between ByVal and ByRef because they are used in parameter lists. Understanding these keywords will help you understand how to use a subroutine to change the value of a variable that is defined in some other section of the program. Please, I beg you, reread this material until you are sure you understand it. It will make life a lot easier later on if you take the time to understand it now!