Now that
you can write subprogramsincluding subroutine and function procedures stored in modulesit's time to learn how those routines can communicate with each other. Until now, all variables that you've defined have been local
variables. A
local variable is known only within the procedure in which you define the variable. Therefore, if one procedure calculated a variable and a second procedure needed to work with that same variable, the two procedures would have no way to
share that
variable in a way that both could work with the variable.
Until now, if two or more procedures had to work with a value, that value had to be a value from a control on the form. Controls are available to all procedures within the application.
There won't always be controls for all values, however, especially
the intermediate values needed in many large applications. Only those values displayed to the user should be stored in controls. The remaining intermediate values that you'll work with
must stay in variables and be available to all procedures that work
with those variables.
Concept: There are three kinds of variable scope: local, module, and global. This section explains how to define variables that fit in any of those three kinds of scope.
Definition: Scope determines how much of a program can access a variable.
You already know about all the data types that variables can take on. There are
integers, strings, and decimal variables, as well as variant variables that can accept any data type. Not only do variables have names, contents, and data types, but
variables also take on one of these three kinds of scope:
Note: If you were to look at CONSTANT.TXT, the module file that you added to AUTOLOAD.MAK, you would see that all of its named values are defined inside the (general) procedure. The location of those definitions and their definition using the Global statement dictate that all values inside CONSTANT.TXT are defined at the module level. If Microsoft defined all of CONSTANT.TXT's named constant values from within a CONSTANT.TXT procedure other than (general), no other procedure could access the values within CONSTANT.TXT.
When you need to define variables, you should no longer automatically use the Dim statement. Now that you know how
to write programs that contain several procedures, one or more of which might need to share data, you'll need to decide not only
how you want to define the variables (using Dim or Global), but also where you want to define those variables.
Your choice depends on the scope that the variables need. If a variable is used only within a single procedure, there is no reason
to define that variable to have module or global scope.
Review: The
scope of data determines the extent to which other parts of the program can access that data. Controls always have global scope because any procedure in any of a program's modules can use control
data. Variables defined in a module's (general) procedure
using the Global keyword are also global and available from everywhere in the program. Variables defined in the (general) procedure using Dim are module variables and are available from any
procedure in the module. Variables defined using Dim inside
procedures (other than the (general) procedure) are local variables and are available only within that procedure.
Concept: The Global statement defines global variables. You can use Global only within a module's (general) procedure. Often, programmers add the Const keyword, as done throughout CONSTANT.TXT, to define
named
constant values that are variables whose values can't change.
The Global statement is similar to Dim. Here is the format of Global:
Global [Const] VarNa me [AS DataType] [= value]
You can define variables only as global variables in the (general) declarations procedure of a module. Every procedure within that entire application can access the global variables. Therefore, a module's Form_Load()
procedure can initialize a global
variable that you defined, and another module's function procedure can access and change that same variable. If you use the Const keyword, you must also assign the variable an initial value but not use a data type.
Without Const, you must specify
the data type.
Tip: Define constant names that contain all uppercase letters. Throughout the program, you'll more easily be able to distinguish variable from constant names.
The following statement, appearing in the (general) procedure of any non-form module, defines an integer global variable named MyGlobal. Any procedure within the
entire application can initialize, access, and change the variable.
Global MyGlobal As Integer
The Const keyword enables you to name constants that you'll use throughout the rest of the program.
For example, if your company's number of divisions is eight, you might want to define a global named constant like this:
Global Const NUMDIVS = 8
A named constant can never be changed, by
user input, an assignment, or through any other kind of variable-changing statement for the rest of the program. NUMDIVS will always hold the value of 8. The advantage of using a named constant over the
value of 8 everywhere in the program that needs to
use the number of divisions is that, if the number of divisions change, you have to change the number in only one place, and the rest of the program automatically uses the new value. Also, the Const
specifier keeps you or someone who modifies your code
from accidentally overwriting the value elsewhere in the code. Figure 16.1 shows the error message box that Visual Basic displays if any line in the program attempts to change a named constant.
Figure 16.1. Visual Basic
reminds you if you attempt to change a named constant.
The reason that you must specify an initial constant value is because the Global statement is the only place in a program where you can assign values to constants.
Stop and Type: Listing 16.1 contains three global variable and two global named constant definitions. The code must appear in the (general) procedure of a non-form module or Visual Basic will issue an error
message.
Review: The Global statement enables you to define global variables. If you use the Const modifier to name global constants, you can't define the global's data type, but you must initialize the
global constant
at the time that you define the named constant.
Listing 16.1. Defines five global values.
1: Option Explicit 2: 3: ' Three global variables 4: Global G1 As Single 5: Global G2 As Double 6: Global G3 As Single 7: 8: ' Two named constants 9: Global Const G4 = 495.42 10: Global Const G5 = 0
Analysis: Line 1 specifies that all variables within
the module must be explicitly defined. The Option Explicit statement means that there's little chance that you'll misspell a variable name. Lines 4 through 6
define three global variables. Any procedure in the entire program, even procedures in other
modules, can initialize, read, and change G1, G2, and G3. Lines 9 and 10 define two globally named constant values called G4 and G5. Both of the constants are
defined in lines 9 and 10 because G4 and G5 can't be initialized anywhere else in the program.
Concept: Module variables are also defined in (general)
procedures. Rather than use Global, you use Dim to define module variables. Module variables are available to all procedures within the module in which
they are defined. Therefore, module variables are more limited in scope than global variables because
they aren't available globally.
Module variables are considered slightly safer variables than global variables. Only the module in which you define module variables can access and change those variables. Therefore, technically, you could have two
different modules in a single
application, and each module could have the same module variable with the same name. Each module's module variables, however, would be separate variables. The code that accesses and changes the module variable within one of
the modules changes only that
module's variable.
Caution: Despite the fact that two modules within the same application can be named the same variable name without conflict, you should try not to duplicate variable names because of the maintenance confusion such double-defined variables can cause.
Unlike global variables, which you can define only in non-form
modules, you can define module variables in the form's module or within any external module in your program. Generally, programmers rarely use module variables; most applications need either
global values that are available through the application or
local values that are specifically available only to individual procedures.
Stop and Type: Listing 16.2 defines two module variables. The module variables must be defined in the (general) procedure of
any module within the application.
Review: Module variables, defined with Dim in the (general) procedure of any module, are available to any procedure within that module.
Listing 16.2. Code that defines two module variables.
1: Dim M1 As Integer 2: Dim M2 As Double
Analysis: Lines 1 and 2 define two
module variables named M1 and M2. The definitions must appear within some module's (general) procedure. The variables aren't global because Dim defines them instead of Global.
Concept: Local variables are available only within the procedures in which they are defined. Use the Dim statements
to define local variables. You may define local variables only within individual
non-declarations procedures; you can't define local variables within the (general) procedure.
Global and module variables aren't considered as safe as local
variables. Suppose that only three procedures within a large application need access to a variable. If you limit that variable's scope to those three procedures, you won't accidentally
change those variables in other procedures by using the variables
where you never intended to use them.
Most of your program's working variables should be local. You'll define those variables, as you've been doing throughout this book, and those variables aren't available outside their procedures. All of the
Dim statements that you saw in this book,
before this unit, defined local variables.
Caution: Local variables defined with Dim last only as long as the procedure's execution lasts.
When a procedure ends, its local variables defined with Dim go away and their values disappear. If execution were to re-enter that procedure, Visual Basic would define those variables
all over again and initialize them again. Therefore, if execution
enters a procedure a second time, the values of all local variables defined with Dim the last time that the procedure executed would no longer be in effect.
Stop and Type: Listing 16.3 shows a command button's event procedure that defines a local variable and uses that variable and a module variable to compute a value for a label's caption.
Review: When you declare a variable at the top of a procedure using Dim, you declare a local variable that Visual Basic recognizes only for the life of that procedure.
Listing 16.3.
Using a module and a local variable within a procedure.
1: Sub cmdInvFactor_Click() 2: ' Adds an inventory factor to a module-level 3: ' inventory total when the user clicks the 4: ' inventory command button 5: Dim FactAdd As Single 6: FactAdd = .13 7: ' Add to the module variable named InventoryTotal 8: lblInvent.Caption = InventoryTotal + FactAdd 9: End Sub
Analysis: Line 1 begins a new
event procedure. Therefore, any variable defined within the procedure, using Dim, must be a local variable. Line 5 defines the local variable named FactAdd. Line 8 then adds
FactAdd to a module variable named InventoryTotal to compute the final label's
result.
Concept: Local variables are considered the safest,
and generally the best kinds of variables to define. However, there will be many times when a subroutine or function procedure needs a value from another's
local variable. For example, suppose that one procedure calculates a value, and a second procedure
must use that value in a different calculation before displaying a result on the form. This section explains how to pass local data from the procedure
that defines the local variable to other procedures that need to work with that value.
When you call the built-in functions, you pass one or more arguments to the function so that the function's internal code has data to work with. When you call your own subroutine and function procedures, you also can pass arguments to them. The
arguments are nothing more than the passing procedure's local variables that the receiving procedure needs to work with.
Once you pass data, that data is still local to the original passing procedure, but the receiving procedure has the
opportunity to work with those values. Depending on how you pass the arguments, the receiving procedure might even be able to change
those values so that when the passing procedure regains control, its local variables have been modified.
Figure
16.2 illustrates the terminology used in passing and receiving arguments from one procedure to another. A procedure, referred to as the calling procedure, passes one or more of its local variables, referred to as arguments, to the
receiving
procedure. The only reason that parentheses exist following a procedure name is to hold the arguments sent to the receiving function.
Figure 16.2. Showing the proper procedure-calling terminology.
Note: As you already know, if the receiving procedure is a function procedure, the function procedure usually returns a value to the calling procedure.
In Figure 16.2, the receiving arguments are I, J, and K. The receiving procedure uses the same names are the calling procedure in this figure, and that's usually the case. However, you don't have to use the same names. In
other words, if RecProc()
received the three variables as X, Y, and Z, then RecProc() would have three variables to work with named X, Y, and Z, and those three variables would have the same values as I, J, and K in the calling procedure. Therefore, the
receiving names don't have
to match the passed names, although they typically do to eliminate any confusion.
Declare the data type of all received arguments. If you must pass and receive more than one argument, separate the passed arguments and
the received arguments (along with their declared data types), with commas. The following statement passes the three
values to the subroutine in Figure 16.2:
Call RecProc(I, J, K)
The following
statement begins RecProc() procedure:
Sub RecProc (I As Integer, J As Integer, K As Single)
The calling procedure already knows the data types of I, J, and K, but those values are unknown to
RecProc(). Therefore, you'll have to code the data type of each received argument so that the receiving function knows the data type of the sent
arguments.
If a subroutine or function procedure is to receive arrays, don't indicate the
array subscripts inside the argument list. The following Sub statement defines a general-purpose subroutine procedure that accepts four arrays as arguments:
Sub WriteData (CNames() As String, CBalc() As Currency, CDate() As Variant, CRegion() As Integer)
The built-in UBound() function returns the highest subscript that's defined for any given array. The following statement, which might appear inside the WriteData() subroutine, stores the highest
possible subscript for the CNames() array, so the
subroutine won't attempt to access an array subscript outside the defined limit:
HighSub = UBound(CNames)
Warning: Don't forget that the Call statement is funny about the argument parentheses. If you use Call, you must also enclose the arguments in parentheses. You may omit the Call keyword, but if you do, omit the parentheses as well. Here is an equivalent Call statement as that shown in Figure 16.2:
RecProc I, J, K ' No Call, no parens!
Stop and Type: Suppose that
you're writing a set of programs for a bookstore's inventory and customer tracking purposes. The owners of the bookstore require that the user enter a category code that describes
the kind of item being tracked or bought, and the program will print the
description of that category as the purchase is entered or as the item is tracked through inventory. In other words, you'll ask the user for a category code of 1, 2, 3, 4, or 5, and
the program will return a description in a string message variable
describing that category. The following Select Case would work:
Select Case CatCode Case 1: CatMessage = "Book" Case 2: CatMessage = "Magazine" Case 3: CatMessage = "Newspaper" Case 4: CatMessage = "Writing Supplies" Case 5: CatMessage = "Software" Case Else: CatMessage = "The category code is in error" End Select
The problem is that this lengthy Select Case is needed
throughout the program and needed by several other Visual Basic programs that you're writing as well. You're much better off storing this code as a general-purpose function procedure such as the one
shown in Listing 16.4, and calling the function from
wherever the code is needed, like this:
CatDesc = DispCatCode$(CatCode) ' CatDesc is a string
The function procedure enables you to pass the category code number argument and return the string
description that matches that argument. As long as you store the procedure in a module file, any application that you add the module file to will be able
to call the function and receive the message.
Review:
By passing arguments between function and subroutine procedures, your procedures can share local data. If a procedure doesn't need access to another's local variable, you'll never pass that procedure
the variable. By passing data only as
needed, you'll be provided data access on a need-to-know basis; that is, only those procedures that need access to data get access. If all of your variables are global, any procedure could inadvertently change
another's variable, and such logic errors are
often difficult to find.
Listing 16.4. A general-purpose function procedure that you can call from any other procedure.
1: Function DispCatCode$(CatCode As Integer) 2: Dim CatDesc As String 3: Select Case CatCode 4: Case 1: 5: CatMessage = "Book" 6: Case 2: 7: CatMessage = "Magazine" 8: Case 3: 9: CatMessage = "Newspaper" 10: Case 4: 11: CatMessage = "Writing Supplies" 12: Case 5: 13: CatMessage = "Software" 14: Case Else: 15: CatMessage = "The category code is in error" 16: End Select 17: DispCatCode = CatMessage ' Returns a description 18: End Function
Analysis: The DispCatCode$() function could be stored in an external module file along with several other subroutine and function procedures that your applications often need. The module file acts like a
toolchest with
tools (procedures) that you can use (call) any time you need them.
Line 1 defines the procedure to be a function that receives one integer argument from the calling code. Line 2 defines a local string variable that the Case statements will use as
a temporary storage location for the appropriate description. Lines 4
through 15 then test the passed integer to see whether the category code is 1, 2, 3, 4, 5, or another value that would indicate an error. Line 17 ensures that the appropriate message is
returned to the calling procedure.
Concept:
There are two ways to receive passed arguments: by address and by value. The method that you use determines whether the receiving procedure can change the arguments so that those changes remain in
effect after the calling procedure regains
control. If you pass and receive by address (the default method), the calling procedure's passed local variables may have been changed in the receiving procedure. If you pass and receive by value, the calling
procedure can access and change its received
arguments, but those changes don't retain their effects in the calling procedure.
Subroutines and functions can always use their received values and also change those arguments. If a receiving procedure changes one of its arguments, the
corresponding variable in the calling procedure is also changed. Therefore, when the
calling procedure regains control, the value (or values) that the calling procedure sent as an argument to the called subroutine may be different than
before the call.
Arguments are passed by address, meaning that the passed arguments can be changed by their receiving procedure. If you want to keep the receiving procedure from being able to change the calling procedure's arguments, you
must pass the arguments
by value. To pass by value, precede any and all receiving argument lists with the ByVal keyword, or enclose the passed arguments in parentheses.
Tip: It's generally safer to receive arguments by value because the calling procedure can safely assume that its passed values won't be changed by the receiving procedure. Nevertheless, there may be times when you want the receiving procedure to permanently change values passed to it, and you'll need to receive those arguments by address.
Stop and Type: Listing 16.5 shows two subroutine
procedures. One, named Changes(), receives its arguments by address. The second procedure, NoChanges() receives its arguments by value. Even though both
procedures multiply their arguments by ten, those changes affect the calling procedure's variables
only when Changes() is called but not when NoChanges() is called.
Review: The method by which you send and receive arguments determines how long a receiving procedure's changes stay in effect. If the
receiving procedure changes one or more of its received-by-address
arguments, those changes remain in effect when the calling procedure regains control. Therefore, when calling a procedure that accepts arguments passed and received by address, be aware
that the passed values could be different when control returns to the
calling procedure.
Listing 16.5. One procedure receives by address and the other receives by value.
1: Sub Changes (N As Integer, S As Single) 2: ' Receives arguments by address 3: N = N * 2 ' Double both 4: S = S * 2 ' arguments 5: ' When the calling routine regains control, 6: ' its two local variables will now be twice 7: ' as much as they were before calling this. 8: End Sub 9: 10: Sub NoChanges (ByVal N As Integer, ByVal S As Single) 11: ' Receives arguments by value 12: N = N * 2 ' Double both 13: S = S * 2 ' arguments 14: ' When the calling routine regains control, 15: ' its two local variables will not be 16: ' changed from their original values. 17: End Sub
Analysis: Line 1 defines the Changes() procedure to receive two arguments by address. Therefore,
when lines 3 and 4 double those two values, the calling procedure's variables will be doubled also.
Line 10 defines the NoChanges() procedure that receives its two arguments by value. The ByVal keyword tells Visual Basic that no matter what
happens to the arguments in the NoChanges() procedure, the calling procedure's values will be unaffected by the
change.
The following statements would call these two procedures properly:
Call Changes(N, S) Call NoChanges(X, S)
Again, the variable names in the calling procedures do not have to match the corresponding received names in the receiving argument lists. Therefore, the calling procedure sends its local variable named X to
NoChanges(), which references that value as N
inside the receiving procedure. If you want to send data to either procedure and ensure that the sent variables can't be changed even in the procedure that receives by address, enclose each sent
argument in parentheses like this:
Call Changes( (N), (S) ) Call NoChanges( (X), (S) )
The special parentheses coding overrides any by address passing and ensures that the passed values will retain
their original values after the called procedures finish.