X# provides support for dynamically scoped variables that are created and maintained completely at runtime.
The term dynamically scoped refers to the fact that the scope of these variables is not limited by the entity in which the variable is created.
Warning! Dynamically scoped variables are NOT supported in the Core and Vulcan dialects. In other dialects they are supported only if the -memvar compiler option is enabled.
Variable Type |
Lifetime |
Visibility |
PRIVATE |
Until creator returns or until released |
Creator and called routines |
PUBLIC |
Application or until released |
Application |
The data type of a dynamically scoped variable changes according to the contents of the variable. For this reason they are often described as dynamic or polymorphic.
Dynamically scoped variables are provided mainly for Clipper/Xbase compatibility; however, they are very useful in certain circumstances. For instance, they let you develop rapid prototypes and have certain inheritance properties that you may find hard to resist.
You must be aware, however, that using them comes at a cost. Consider these points:
•Because they are not resolved at compile time, these variables require overhead in the form of runtime code, making your application larger and slower than necessary.
•No compile time checking for type compatibility is possible with these variables.
•Using the inheritance properties of these variables defies one of the basic tenets of modular programming and may lead to maintenance and debugging problems down the line. Furthermore, this practice will make the transition to lexically scoped and typed variables more difficult.
This section explores dynamically scoped variables fully, but X# has several options for variable declarations that you will want to explore before choosing to use this variable class. The next two sections in this chapter introduce you to Lexically Scoped Variables and Strongly Typed Variables, which you may find useful.
Important! For the sake of illustration, some of the examples in this section use unorthodox programming practices. Using the inheritance properties of public and private variables instead of passing arguments and returning values is not recommended.
Private is one of the two types of dynamically scoped variables, and there are several ways to create a private variable:
•List the variable name as part of a PRIVATE statement. If you do not make an assignment at this time, the variable takes on the NIL value and data type; otherwise, it takes on the data type of its assigned value. You can assign a new value (with a new datatype) to a variable at any time:
PRIVATE X := 10, y
This creates x as a numeric variable and y as an untyped variable with the value NIL. You can later change the values and their types by assigning other values to them:
X := "X# Is great"
Y := Today()
•List the variable name as part of a PARAMETERS statement within a FUNCTION, PROCEDURE or METHOD definition. The variable takes on the data type of its associated argument when the routine is called, or NIL if the argument is omitted. You can assign a new value (and a new data type) to the variable at any time.
•Assign a value to a non-existent variable name (for example, x := 10). The variable takes on the data type of its assigned value until you assign a new value to it. (x is numeric, but the assignment x := "Ms. Jones" changes it to a string.) This will only work if you have used the -undeclared as well as the -memvar commandline options.
Private variables have these properties:
•You can access them within the creating routine and any routines called by the creator. In other words, private variables are automatically inherited by called routines without having to pass them as arguments.
•You can hide them from a called routine by explicitly creating a private (using PRIVATE or PARAMETERS) or declaring a local (using LOCAL) variable with the same name in the called routine.
•They are automatically released from memory when the creator returns to its calling routine, or you can release them explicitly using RELEASE, CLEAR ALL, or CLEAR MEMORY.
In this example, the function Volume() expects three arguments, or parameters, to be passed. When the function is called, it creates three private variables, nLength, nWidth, and nHeight to accept the arguments. Because they are created with the PARAMETERS statement, any higher-level variables (either public or private) created with these names are temporarily hidden, preventing their values from being overwritten in memory:
FUNCTION Volume()
PARAMETERS nLength, nWidth, nHeight
RETURN nLength * nWidth * nHeight
In the next example, a modified version of Volume() creates a private variable (assuming no other variable name nVolume is visible) to store its return value. If the variable nVolume exists prior to calling Volume() and is visible to Volume() (for example, nVolume may be public or private to the routine that called Volume()), its value is overwritten in memory and will remain changed when the function returns to its calling routine:
FUNCTION Volume()
PARAMETERS nLength, nWidth, nHeight
nVolume := nLength * nWidth * nHeight
RETURN nVolume
In this version, Volume() specifies the nVolume variable as PRIVATE. Doing this temporarily hides any higher-level variable (either public or private) with the same name, preventing its value from being overwritten in memory:
FUNCTION Volume()
PARAMETERS nLength, nWidth, nHeight
PRIVATE nVolume := nLength * nWidth * nHeight
RETURN nVolume
The second category of undeclared variable is public. Public variables have application-wide lifetime and visibility, and you can define them in only one way:
•List the variable name as part of a PUBLIC statement. If you do not make an assignment at this time, the variable takes on a value of FALSE (or NIL for array elements); otherwise, it takes on the data type of its assigned value. You can assign a new value (and a new data type) to the variable at any time.
Public variables have these properties:
•Once they are created, you can access them anywhere in the application. In other words, public variables are automatically inherited by all routines in the application without having to pass them as arguments or post them as return values.
•You can hide them from a routine by explicitly creating a private (using PRIVATE or PARAMETERS) or declaring a local (using LOCAL) variable with the same name.
•They are not released from memory until you explicitly release them using RELEASE, CLEAR ALL, or CLEAR MEMORY.
In this example, the function Volume() is defined without arguments. Instead, the calling routine, Compute(), creates three public variables, nLength, nWidth, and nHeight that are automatically visible to Volume():
PROCEDURE Compute()
PUBLIC nLength := 5, nWidth := 2, nHeight := 4
? Volume() // Result: 40
FUNCTION Volume()
RETURN nLength * nWidth * nHeight
In the next example, a modified version of Volume() creates a public variable to store the computed volume, getting around having to return a value to the calling routine. Since nVolume is public, it is not released from memory when Volume() returns:
PROCEDURE Compute()
PUBLIC nLength := 5, nWidth := 2, nHeight := 4
Volume()
? nVolume // Result: 40 , this will only compile with -undeclared
RETURN
PROCEDURE Volume()
PUBLIC nVolume
nVolume := nLength * nWidth * nHeight
RETURN
A better solution for the use of the nVolume variable from last example that will not require the -undeclared commandline option is:
PROCEDURE Compute()
PUBLIC nLength := 5, nWidth := 2, nHeight := 4
MEMVAR nVolume // tell the compiler that nVolume is a Public or private
Volume()
? nVolume // Result: 40
RETURN
or
PROCEDURE Compute()
PUBLIC nLength := 5, nWidth := 2, nHeight := 4
Volume()
? _MEMVAR->nVolume // Result: 40
RETURN
Please note that this kind of programming is NOT recommended.
Once a public or private variable is created as demonstrated in the previous two sections, you obtain its value by referring to its name. You might display the value of a variable using a built-in command or function:
? nVolume
QOut(nVolume)
or use its value as part of an expression:
Str(nVolume, 10, 2) + " cubic feet"
For dynamically scoped variables, you can use the _MEMVAR alias to qualify a variable reference. In some cases, you may have to do this in order to help the compiler resolve what might otherwise be an ambiguous reference (for example, if you have a field variable with the same name as a memory variable and want to use the memory variable in an expression).
Note: MEMVAR is an abbreviation for memory variable, a term that is synonymous with dynamically scoped variable.
Assuming that the database file Measures has fields named nLength, nWidth, and
nHeight, this example calls Volume() using the field variable values:
FUNCTION Calculate()
PRIVATE nLength := 5, nWidth := 2, nHeight := 3
USE measures
? Volume(nLength, nWidth, nHeight)
...
To force the function to use the private variables instead of the field variables,
you could use the _MEMVAR-> (or, more simply, M->) alias to qualify the
variable names:
FUNCTION Calculate()
PRIVATE nLength := 5, nWidth := 2, nHeight := 3
USE measures
? Volume(_MEMVAR->nLength, _MEMVAR->nWidth, _MEMVAR->nHeight)
...
Of course, it is better to avoid ambiguous situations like the one described above by taking care to have unique field and variable names, but the point is that the compiler has certain default rules for handling ambiguous references. If you do not want to be at the mercy of those defaults, it is best to qualify variable names in all cases.
Although you may hear them referred to as such, the statements mentioned so far in the discussion of dynamically scoped variables are not declarations. The term declaration refers to a statement whose purpose is to inform the compiler of something—PRIVATE, PARAMETERS, and PUBLIC are statements that generate memory variables at runtime.
In fact you never have to declare a dynamically scoped variable to the compiler, which is the reason for their inefficiency. Because they are not created using compile-time declaration statements, the compiler has to generate runtime code for handling such issues as type translation, memory management, and resolving ambiguous references to variable names since it is possible for several variables with the same name to be visible at one time.
You can, however, declare dynamically scoped variables with the MEMVAR statement and they will be created as PRIVATE variables:
FUNCTION Calculate()
MEMVAR nLength, nWidth, nHeight
nLength := 5
nWidth := 2
nHeight := 3
USE measures
? Volume(nLength, nWidth, nHeight)
In this case, the MEMVAR statement causes memory variables to take precedence over field variables with the same names, causing Volume() to be called with the private variables.
Using MEMVAR to declare dynamically scoped variable names to the compiler may make your programs slightly more efficient (especially if you have lots of ambiguous references); however, it will not eliminate the runtime overhead of these variables.