Xbase Xplained
To him who looks upon the world rationally, the world in its turn presents a rational aspect. The relation is mutual.
Georg Wilhelm Friedrich Hegel, Philosophy of History, 1832
FoxPro's oldest heritage is its Xbase heritage. As we said in "It's Always Been That Way," Xbase has a long and varied history. This section explains the basic language concepts that derive from that history. dBase was originally conceived primarily as an interactive language. Because of this, many of the older commands' default behaviors focus on interactive results, with optional clauses available to use the command programmatically.Do You Work Here?
The nature of a relational database like FoxPro is that you need to work with many tables at once. You couldn't get much work done if you had access to only one table at a time. Xbase handles this by providing multiple work areas, each capable of handling one table and its related indexes. The available number of work areas has been one of the carrots manufacturers have used in the Xbase competition. In ancient Xbase history, there were two work areas. By dBase III Plus and FoxBase+, 10 work areas were provided and each table could be opened with up to seven index files. In FoxPro 2.5, the number of available work areas went up to 225. Since you'd run out of file handles long before you could open 225 different tables, you'd think that would be sufficient for anyone's needs, even if some tables are open several times. But Visual FoxPro raised the stakes again, providing 32,767 work areas. Yes, 32K work areas! (In fact, there are 32,767 work areas per data session. More on data sessions later.) While this seems extravagant (in fact, ridiculous) at first glance, it leaves room for a lot of things going on behind the scenes that weren't there before. Visual FoxPro's buffering and multiple data sessions require a lot more work areas than ever before. Remember that each file still needs one or more file handles (See "Hardware and Software Recommendations"), so while you can open the same tables many times, you still can't open 400 tables at once—not that you'd ever want to.It's also interesting that Visual FoxPro's databases, a much newer concept than tables, don't require work areas. We guess Microsoft found a better way to handle this internally. There's no documented limit on how many databases you can open simultaneously, but databases require file handles (three, actually—one each for DBC, DCT and DCX) so there is presumably some limit, even in the most recent versions of Windows.Work areas are numbered from 1 to 32767 and can be referenced by number, as in:SELECT 27Doing so is usually a bad idea, except when hacking from the Command Window. Similarly, it is poor form to refer to the first 10 work areas by the letters A–J, a historical vestige of the days when there were only 10 work areas.Once a table is open in a work area, you should refer to that work area by the table's alias, like this:
SELECT CustomerBefore that, it doesn't matter which work area you use. You can get to the lowest available work area by issuing:
SELECT 0or to the highest available work area (why? we dunno, but you can) with:
SELECT SELECT(1)To open a table in the lowest available work area, there are two choices. As above, you can use SELECT 0:
SELECT 0 USE MyTableor you can incorporate the 0 in the USE command:
USE MyTable IN 0There is one subtle (and undocumented) difference between those two sequences. The first leaves you in the work area containing the newly opened MyTable. The second leaves you in whatever work area you were in before. It opens the table, then returns to the current work area. We can't tell you how many times we've fallen into this trap.As the previous paragraph implies, you're "in" one work area at a time. That is, one work area is always current. You can refer to fields of the table open in that work area without aliasing them. Also, many commands are scoped to the current work area. To make a different work area current, you SELECT it, preferably by using the alias of the table open in that work area:
SELECT MyOtherTableTo close the table open in a work area, issue USE with no alias:
USE && closes the table in the current work areaOr specify an alias to close that one:
USE IN MyTable && closes the table opened with alias "MyTable"This last example points out that it's actually less and less necessary to switch to a particular work area. In recent versions, virtually all of the Xbase commands have an IN clause that lets you specify to which work area a command applies. Rather than saving the current work area, selecting another, doing whatever you need to do, and then returning to the original work area, you can just do the task IN the relevant work area.
So What's Your Point?
Xbase (unlike SQL) is a record-based language. Just as Xbase always believes one work area is the "current" area, one record in each table is also "current." So, in each open table, Xbase maintains a record pointer. The record pointer always points to a single record. Many commands operate on the record pointed to.Lots of Xbase commands move the record pointer. Some are specifically designed to move the pointer, while others do it more or less as a side effect. The second type is discussed in the next section, "Scope, FOR, WHILE (and Santa) Clauses."Commands designed to move the record pointer can be further divided into two groups: those that move it based on position and those that move it based on content. GO and SKIP fall into the position category, while LOCATE, CONTINUE and SEEK are in the content category. Both GO and SKIP are affected by an active index order. If you've SET ORDER TO something other than 0, GO TOP and GO BOTTOM move to the top and bottom of the current order, respectively. (However, they can do so pretty slowly. See "Faster than a Speeding Bullet" for hints on doing this better.) Only GO <n> is absolute. It moves to the record whose record number is <n>, regardless of any order. This, of course, makes GO <n> nearly useless. Its only real utility is in returning the record pointer to a previous position after a command has moved it. For example:nHoldRec=RECNO() COUNT FOR category="Oddities" GO nHoldRecUnfortunately, even that simple piece of code won't always work. If the record pointer is at EOF() before the COUNT, the GO fails. Instead, you have to do it like this:
nHoldRec=IIF(EOF(),0,RECNO()) COUNT FOR category="Oddities" IF nHoldRec=0 GO BOTTOM SKIP ELSE GO nHoldRec ENDIFExcept in the situation above, stay away from referring to records by their record numbers. Record numbers are a volatile thing. If you delete some records and pack the table, the record numbers change. The real point is that you don't need record numbers. FoxPro has plenty of other tools you can use to find a particular record.
Scope, FOR, WHILE (and Santa) Clauses
Xbase commands operate on groups of records—a group may be as small as one record or as large as an entire table. There are three ways to specify which records are affected by an Xbase command. They are Scope, the FOR clause and the WHILE clause. All three reflect the record order-based nature of Xbase processing.Almost all the record processing commands that originated in Xbase accept all three clauses. The group includes DELETE, RECALL, REPLACE, LIST, DISPLAY, AVERAGE, COUNT, SUM, CALCULATE, LABEL FORM, REPORT FORM, SCAN and LOCATE, as well as some others.The next few sections describe each of the record-selection mechanisms. Then, "Combining the Clauses" talks about their interactions.A Grand Scope
The designers of Xbase provided several ways of choosing a group of records based on their position in the table: choosing all the records in a table, choosing a single record, choosing several records in sequence, and choosing all the records from the present location to the end of the table. They grouped these four choices together and called them "scope." You specify them by including the appropriate clause, respectively: ALL, RECORD <n>, NEXT <n>, or REST. Note that <n> differs in each context: It means a record number for RECORD, but means "how many" for NEXT. Also, be aware that NEXT and REST are interpreted according to the current index order. Commands that accept a scope clause use a default scope if no scope term is included. Those commands that have the potential to be destructive default to NEXT 1. Commands that can't do any damage to the database's records generally default to ALL. For example, DELETE and REPLACE have a default scope of NEXT 1, while LIST and REPORT default to ALL.You'll find that you use ALL and NEXT quite a bit, while RECORD and REST seem somewhat antiquated in a set-based world. In fact, there are some good times to use REST, but we haven't used RECORD in years.FOR Better or FOR Worse
The FOR clause allows a command to process records based on content, regardless of the position of the record in the table. Prior to FoxPro 2.0, this made a FOR clause something to be avoided at all costs. With Rushmore optimization, FOR clauses can be very fast. (That's "FOR better.") However, if a FOR expression can't be optimized, it can ruin performance. (That's "FOR worse.")FOR accepts a logical expression and processes all records for which that expression is true. This makes it just the thing for getting a list of all customers in the U.K.:BROWSE FOR Country = "U.K."or for replacing all occurrences of a misspelled string with the correctly spelled version:
REPLACE LastName WITH "Quayle" FOR LastName="Quail" AND NOT IsBirdNote that giving REPLACE a FOR (or WHILE) clause changes its default scope to ALL.Any command including a FOR clause may be optimizable. See "Faster Than a Speeding Bullet" for hints on how to optimize these commands.
WHILE Away the Hours
WHILE is used when records are already ordered the way you want. It processes records starting with the current record and continues until it encounters a record that doesn't meet the condition.WHILE is handy for things like totaling all the detail records of an invoice:SELECT Detail SET ORDER TO InvId SEEK Invoice.InvId SUM DetAmt WHILE InvId=Invoice.InvId TO InvTotalor changing the area code for all the people with a particular phone exchange:
SELECT People * Tag Phone is on the actual phone number field; * area code is a separate field. SET ORDER TO Phone SEEK "555" REPLACE AreaCode WITH "610" WHILE Phone="555"Our first version of the "SEEK, then REPLACE WHILE" example ran into a nasty, subtle problem in working with ordered data. We planned to show changing the ZIP code field for everyone in a particular ZIP code (something that actually happened to Tamar a few years ago). The code looks like this:
SET ORDER TO Zip SEEK "19117" REPLACE Zip WITH "19027" WHILE Zip="19117"But, using this approach, you can't do that. With order set to ZIP code, once you change the ZIP code field for a record, it moves to its new position. In the example, as soon as one record gets the new ZIP code, it moves to join the other "19027"s. Unless you're particularly lucky, when the record pointer moves forward, it's no longer on a "19117" record and the REPLACE ends. You can run into the same problem when you SCAN with order set.To do the ZIP code replacement, you need to use REPLACE FOR instead:
REPLACE Zip WITH "19027" FOR Zip="19117"When you use WHILE, you need to position the record pointer at the beginning of the group of records you want to process. When you finish, the record pointer is on the first record that doesn't match the criteria.
Combining the Clauses
Scope, FOR and WHILE can be combined in a single command. The interactions are logical, but not necessarily intuitive.First, realize that FOR has an implied (or default) scope of ALL. Any command containing FOR without a scope clause or a WHILE clause checks all records. This means it's not necessary to use the ALL keyword here:REPLACE ALL vegetable WITH "Lettuce" FOR vegetable = "Broccoli"If no other scope is included, and there's no WHILE clause, the record pointer is left at EOF() after a FOR clause.WHILE has an implied scope of REST, but it may stop short of the end of the file if the WHILE condition is no longer met.So, what happens when you combine these babies? Adding scope to either FOR or WHILE means that it behaves as it usually does, but processes records only in the specific scope. So:
BROWSE NEXT 20 FOR State = "Confusion"displays all of the next 20 records that meet the FOR criteria, but shows matches only from among those 20, even if fewer than 20 are included.Combining FOR and WHILE seems contradictory, but actually it's not. The key to understanding such a command is knowing that the WHILE is in charge with the FOR just helping out. In other words, we process records until we find one that fails the WHILE test, but we process only those that pass the FOR test. For example:
SELECT Employee SET ORDER TO GroupId SEEK " 2" LIST First_Name, Last_Name WHILE GroupId = SPACE(5)+"2" ; FOR NOT EMPTY(Photo)lists only those employees in Group 2 for whom we have a photo on file.As we mentioned above, a few commands accept only FOR and not WHILE. LOCATE accepts both a FOR and a WHILE clause, but the FOR clause is required in that case. That is, to find Group 2 employees with photos, you can:
LOCATE FOR NOT EMPTY(Photo) WHILE GroupId = SPACE(5)+"2"but you can't use WHILE to look for all the Group 2 people, like this:
LOCATE WHILE GroupId = SPACE(5)+"2"This behavior makes some sense. Using just WHILE with LOCATE isn't very useful. You might as well use SKIP and test the condition. WHILE requires the records to be in order anyway, so LOCATE WHILE is overkill. However, we might want to search only in a specific group of records for the first record that meets a condition. LOCATE FOR with a WHILE clause does that for us.
Shall I Compare Thee to a Summer Day?
One of the most confusing aspects of Xbase is the way string comparison works. The Xbase way is to compare strings only to the end of the right-hand string. If they match up to that point, they're considered equal. Given Xbase's interactive history, it's not really bad behavior. When you're searching for a particular record or want to find all the records that match a given string, it's kind of nice to not have to type out the whole string to match. But in programs, this behavior can be a disaster.The SET EXACT command controls string comparisons for Xbase commands. For more than you ever wanted to know about this subject, check out SET EXACT in the Reference section.Operator, Operator, Give Me Number Nine
"Reeling and Writhing, of course, to begin with," the Mock Turtle replied, "and the different branches of Arithmetic—Ambition, Distraction, Uglification, and Derision."
Lewis Carroll, Alice's Adventures in Wonderland, 1865
Remember seventh grade (or maybe it was sixth or eighth) when you learned a mnemonic that went something like "Please Excuse My Dear Aunt Sally?" Right about now, you're probably scratching your head and saying, "Oh, sure, those are the Great Lakes, right?," or "Yeah, that sounds familiar, but what's it about?" unless, like us, you remember this kind of stuff forever, and you're sitting there saying "Sure, parentheses, exponents, multiplication, division, addition, subtraction." You got it: The mnemonic provides the order of precedence of numeric operators. That is, when you see:3 + 7 * 4you know that it's 31, not 40, because multiplication comes before addition (except in the dictionary). On the other hand:
(3 + 7) * 4is 40 because parentheses come before any other operators. When there are multiple operators at the same level, they're evaluated from left to right.In Xbase, there are a bunch of other operators besides arithmetic operators. Comparison operators (=, <>, <, >, <=, >=, and $) are used to compare values (big surprise). Logical operators (AND, OR, NOT) are used to combine logical expressions (often comparisons). Some of the operators used for arithmetic can also be applied to other data types (all of them for double, integer and currency, + and - for characters, - for dates and datetimes). There's also one additional arithmetic operator (%, which is the same as MOD()) that applies to numeric, float, double, integer and currency values. The arithmetic precedence rules have been extended to cover the full set of operators. Arithmetic operators used on other types have the same precedence as when they're used with numbers. The % has the same precedence as multiplication and division.The complete precedence rules for FoxPro are somewhat different from those in some other programming languages. In particular, logical operators come below arithmetic and comparison operators:
Just Some Routine Inquiries, Ma'am
A subroutine (or sub-program, as some folks call them) is a program that is called from another program, performs a task, and then returns to the calling program, which picks up where it left off. We like to think of the original routine as getting moved to the back burner until the subroutine finishes executing; then it comes back to the front burner again.Like many programming languages, FoxPro has two kinds of subroutines: procedures and functions. The big difference in using them is that a function returns a value while a procedure doesn't, so a function call can appear as part of an expression while a procedure call cannot.By the way, many people refer to functions and even procedures as UDFs for "user-defined functions." This is to distinguish them from FoxPro's built-in functions. Historically, there have been some places where a built-in function could be used, but a UDF could not, though we can't think of any in Visual FoxPro. In many programming languages, there's a distinction drawn between procedures and functions: A subroutine is one or the other. Not so in FoxPro. Here, any subroutine can be either a procedure or a function. In fact, it's not the name you give the subroutine when you define it (either PROCEDURE MyRoutine or FUNCTION MyRoutine) that determines whether it's a procedure or function; it's the way you call it. That is, the same subroutine can be called as a function sometimes and as a procedure at others. Like this:DO MyRoutine with Param1, Param2or
? MyRoutine(Param1, Param2)Why does this matter? Two reasons. One we already mentioned: Functions return a value. When you call a function as a procedure, the return value is thrown away. (When you call a procedure as a function, and there's no return value, the function returns .T.)Throughout Xbase history, when you called a routine as a function (with the parentheses at the end), you had to do something with the result. You couldn't write:
MyRoutine()Instead, you needed to assign the result to a variable, use it in an expression, display it, or throw it out. Here are examples of each:
MyResult = MyRoutine() && value assigned to MyResult IF MyRoutine() > 7 ... && value tested ? MyRoutine() && value displayed = MyRoutine() && value thrown outThe equal notation in the last example says to run the function and throw away the return value. The only reason to do it that way is if you're running the function for what it does and not for what it returns—that is, for its side effects. (More on side effects later.) Starting in VFP 5, you can cut to the chase and just call the function without doing anything with the result.
Pass the Parameters, Please
The other important distinction between procedures and functions has to do with parameter passing. Parameters are the way that one routine communicates with another. Think of the name of a subroutine as an instruction to do something, and the parameters as the items the instruction applies to. Let's look at a built-in function as an example:UPPER(LastName)says "Apply the UPPER function to the LastName field and return the result." Since UPPER() converts whatever you give it to uppercase, you get back something like this:
"GATES"Sometimes the routine needs several pieces of information to carry out its task. For example, the built-in function SUBSTR() returns part of a character string you pass it (that is, a substring). It accepts two or three parameters: the string to work on, the place the substring starts, and, if you want, the length of the substring. Here's an example:
SUBSTR("The quick Visual FoxPro jumped over the slower dogs", ; 11, 13)returns:
Visual FoxProA subroutine must indicate what parameters it expects to receive. There are three ways to do so: by using the PARAMETERS statement, by using the LPARAMETERS statement, and by listing them in parentheses in the routine's header line (PROCEDURE or FUNCTION declaration). The PARAMETERS statement creates the parameters as private variables, while the other two methods create them as local variables. (See "Scope It Out!" below for the distinction between private and local variables.)There are two ways of passing parameters: by value and by reference. The passing method indicates whether the subroutine can affect the original item passed to the routine. If you want to be technical about it, the parameters specified in the subroutine are called "formal parameters," while the items you pass are called "actual parameters." The method of passing parameters determines whether the actual parameters change when the formal parameters are changed. Got it? Neither did we, nor did our students the first few times. So let's not be technical about it. Think of it this way, instead: When you pass by value, the items you pass are copied to the parameters listed in the subroutine. Any changes to the parameters don't affect the items in the call. When you pass by reference, FoxPro makes an internal connection between the parameters and the items you pass, so changes to the parameters ripple back to the items you passed. In fact, it turns out that the names of the two methods are pretty informative. When you pass by value, the value of the item is passed, not the item itself. When you pass by reference, FoxPro creates a reference to the original item. Better now?Now here's the sneaky part. Ordinarily, you don't explicitly specify the parameter-passing method—it's implied by the subroutine call. Functions pass parameters by value; procedures pass by reference. Remembering that it's the call that determines whether it's a procedure or a function, that's a major implication. The same routine may behave very differently, depending how you call it. Here's a very simple example. We usually check parameters at the beginning of a routine to make sure they're the right type and have realistic values. Suppose the beginning of a subroutine looks like this:
PROCEDURE ProcessDate PARAMETER dInput IF TYPE('dInput')<>"D" dInput=DATE() ENDIF * now do whateverNow look at these two calls to the routine:
MyDate = "Sandy" ? ProcessDate( MyDate )or
MyDate = "Sandy" DO ProcessDate WITH MyDateIn the first case, the variable MyDate isn't affected by the error checking in the routine. In the second case, MyDate gets changed to today's date (and isn't Sandy's mother surprised?).Normally, this isn't a problem because most routines are called either as procedures or as functions, not both. So let's move on to another issue.There's a principle of structured programming that says functions shouldn't have "side effects." This means that functions should accept some parameters, do some processing and return a value—without changing anything in the environment. (It reminds us of the scouting maxim for visiting the wilderness: "Take only pictures, leave only footprints.") This is why parameters are passed to functions by value. (There is one case, discussed below, where it's essential to pass by reference.) Anyway, suppose you don't have any principles and you want to pass a parameter to a function by reference. Can you do it?Of course you can—this is FoxPro, after all. In fact, there are two ways to do it. One is global while the other is local. The global approach uses a SET command—SET UDFPARMS. You can SET UDFPARMS TO VALUE (the default) or TO REFERENCE. When you do so, all subsequent function calls use the specified method.The local approach is to prefix the actual parameter (the item you're passing) with the "@" symbol. For example:
=MyFunction(@MyVariable)calls MyFunction, passing MyVariable by reference.On the whole, we prefer local approaches to local problems. Remember that environmental slogan, "Think Globally, Act Locally"? The same thing applies here. Consider the overall effects of your actions, but take the actions over the smallest possible scope. The problem with SET UDFPARMS is that it can have unexpected effects. Suppose you change it to REFERENCE and call a function. Suppose that function in turn calls another function, which expects its parameters to be passed by value. The inner function may change one of its parameters (not expecting that to affect anything outside the function), which in turn changes the results of the function you called. What a mess.What about the one case where you have to pass by reference? When you specify an array in a function call (with UDFPARMS set to VALUE), only the first element of the array is passed. Makes it a little hard to, say, total the array elements. To pass an entire array to a function, you must pass by reference. So, a call to our fictional array-totaling function would look like:
nTotal=SumArray(@aMyArray)What about the flip side—suppose you want to pass by value to a procedure. Surprisingly, there's only one local way to do it—no equivalent of SET UDFPARMS in sight. All it takes is to turn the actual parameter into an expression. This is best demonstrated by the fact that you can pass constants to a procedure. For example, the following is a valid procedure call:
DO MyProcedure WITH 14, 29, 93, "Hike"as long as MyProcedure expects four parameters.But how do you turn a variable into an expression? Simple: Put parentheses around it and it becomes something that FoxPro evaluates before passing. Voila—passing by value. Here's an example:
DO MyOtherProcedure WITH (MyVariable), (MyOtherVariable)We've noticed in the last few years that more and more programmers (ourselves included) have a tendency to use only functions and not procedures. This is partly a consequence of object-oriented programming (methods, after all, feel an awful lot like functions). We're not sure yet whether this is a good thing or a bad thing. Since many of the routines we write actually do something other than returning a value, calling them as functions certainly does violate the old rule about side effects. If nothing else, be sure to document the effects your code has on the environment.
Two's Company � Three's Even Better
FoxPro has allowed you to pass fewer parameters than a routine expects for quite a long time. The actual parameters are matched to the formal parameters (those declared with a PARAMETERS or LPARAMETERS statement) one by one, and the extra formal parameters at the end get initialized to .F. The PARAMETERS() function (and the superior PCOUNT()) tells you how many parameters were actually passed, so you can give the rest useful defaults.In Visual FoxPro things are even better, though more confusing, too. You can choose to omit actual parameters even if they're not at the end of the list. Just put in a comma placeholder and the corresponding formal parameter gets initialized to .F. While we welcome this change because it means parameters can be grouped logically instead of based on the likelihood of needing them, it also introduces new complexity. It used to be simple. Check PARAMETERS() and you knew that everything after that had defaulted to .F. Now, that's not enough. You have to check each parameter to make sure it's got a value of the right type. Of course, that's a good idea in any case, since you can also pass the wrong type anyway.Mom Was Right
The mess you can cause with SET UDFPARMS leads to an observation about subroutines in general. They should make no assumptions about the environment in which they're called and should clean up after themselves. This means a routine shouldn't expect a certain variable to exist or that any SET commands (like SET EXACT or SET DELETED) will have particular values. If it changes any of those, it should set them back to their original values before exiting. Routines following these rules will be welcome in any programming neighborhood.Can You Offer a Subroutine a Good Home?
There are at least three places you can store subroutines in FoxPro. Each routine can be stored in its own PRG file (for performance implications, see "Faster than a Speeding Bullet"), a routine can be stored with the program that uses it, or you can group a bunch of routines together into a "procedure file" (which can contain functions as well as procedures).Prior to FoxPro 2.0, separate PRG files were too much trouble to manage. You had to remember where possibly hundreds of little programs could be found. So, most people stored subroutines used by only one program with that program, and stored more widely used subroutines in a procedure file. But there were problems with that approach, since only one procedure file could be used at a time, and there was no way to save the name of the current procedure file before switching to another.The Project Manager changed all that. It remembered where you left a function so you didn't have to. Suddenly, keeping every routine in a separate PRG file became feasible. Several well-respected FoxPro developers suggested directory structures to aid in this endeavor. Others offered programs to automate the process of breaking a procedure file into its component parts.With Visual FoxPro, the waters have been muddied. The SET PROCEDURE command now supports an ADDITIVE clause, so multiple procedure files can be set. On the other hand, the Project Manager is still happy to do the work for you. Finally, object orientation means that code is stored with data for anything defined as an object, and the database allows some code to be stored there as well.Our preference is still for stand-alone PRG files for code that isn't object method code.Scope it Out!
Desiring this man's art, and that man's
scope,
With what I most enjoy contented least;
Yet in these thoughts myself almost despising,
Haply I think on thee.
William Shakespeare, Sonnet 29, l. 7
By now, it should be no surprise that the word "scope" has two distinct meanings in FoxPro. One is discussed above in "Scope, FOR, WHILE (and Santa) Clauses." The other (which has nothing to do with the first) applies to visibility of variables.Variables in FoxPro have three possible scopes: public, private and local. They vary as to which routines (other than the one in which the variable was created) can see and change the variable.Doing It in Public
Public variables are exhibitionists. They're visible in every routine in an application except any that have an identically named private or local variable. Anybody can use their value; anybody can change it. Any variable you create in the Command Window is public. Otherwise, to create public variables, you need to use the PUBLIC keyword. In older versions of FoxPro, PUBLIC was picky—if the variable existed when you tried to make it public, you got an error message. Although VFP no longer complains about this, most FoxPro programmers make it a habit to write it this way:RELEASE PublicVar PUBLIC PublicVarThe RELEASE doesn't do any harm if the variable doesn't exist already, but protects you if it does. Variables created this way start out life as logical with a value of .F.Public variables stick around until you explicitly release them. It's not good enough to say RELEASE ALL, either. You've got to list them out, one by one, in order to release them (unless you do it in the Command Window, where RELEASE ALL does release all public variables—go figure). That's as good a reason as any to stay away from public variables.Actually, you should keep public variables to a minimum. As with SET commands, they're often a global solution to a local problem. Frequently, creating a private variable at the appropriate level in your program will give you the same results.There is one "gotcha" involving public variables. If you pass a public variable by reference (see "Pass the Parameters, Please"), that variable is hidden within the called routine. This means a reference to the variable within the called routine gives an error message. This is just another reason to avoid public variables as much as possible.
It's a Private Affair
The term "private variable" is really a misnomer. These variables aren't private. (In fact, local variables behave as the word "private" implies; see below.) Private variables are visible in the routine that creates them and in all routines lower in the calling chain, unless those routines have their own private or local variable of the same name.Let us run that by you again. When you declare a private variable, it hides any variables of the same name that were created higher in the calling chain, but can be seen by routines lower in the calling chain.Hmm, how about an example? Suppose you have a routine MyRoutine that creates a variable called MyVar, then calls a subroutine called MySub. MySub can see MyVar and act on it. However, if MySub declares its own private version of MyVar, MyRoutine's MyVar is hidden and MySub acts only on its own version of MyVar. Here's some code to demonstrate the point:* MyRoutine.PRG MyVar1 = 7 MyVar2 = 10 ? "In MyRoutine before calling MySub", MyVar1, MyVar2 * Note that MyVar1 and MyVar2 are NOT passed as parameters DO MySub ? "In MyRoutine after calling MySub", MyVar1, MyVar2 RETURN PROCEDURE MySub PRIVATE MyVar2 MyVar2 = 2 ? "In MySub before doubling", MyVar1, MyVar2 MyVar1 = 2 * MyVar1 MyVar2 = 2 * MyVar2 ? "In MySub after doubling", MyVar1, MyVar2 RETURNIf you run this program, you'll see that MyVar1 ends up with a value of 14, but MyVar2 (in MyRoutine) remains 10. So, using private variables, you can hide information from a higher-level routine, but not from a lower level.If you don't specify otherwise, variables are created private. Private variables are released when you return from the routine that created them. You can explicitly release them sooner with the RELEASE command.You can explicitly make a variable private by declaring it with the PRIVATE keyword. You must do this if a variable of the same name exists at a higher level in the calling chain. Unlike PUBLIC and LOCAL, though, declaring a variable PRIVATE doesn't create the variable—it just means that if you create that variable, it will be private. This makes some sense when you notice that PRIVATE can take a skeleton and make all variables matching that skeleton private—the others take only a specified list of variables. The skeleton allows you to do things like this:
PRIVATE ALL LIKE j*which means any variable you create beginning with J is private.
Local Initiative
Prior to Visual FoxPro, public and private were the only choices for variables. Visual FoxPro adds local variables, which makes writing black-box code much easier.Local variables can be seen only in the routine that creates them. No other routine can access or change them. This makes them perfect for all those bookkeeping tasks you need in a routine, like loop counters, holding part of a string, and so forth.Here's an example to demonstrate why local variables are much easier to work with than private variables. Suppose you have a routine that sums a specified subset of an array. That is, you pass the array, a starting element and the number of elements to sum, and the function returns the sum of those elements. Here's the function:* ArraySum.PRG * Return the sum of a specified group of elements * of an array. LPARAMETERS aInput, nStart, nNum * aInput = Array to Sum * nStart = Element to start * nNum = Number of elements to sum LOCAL nCnt,nSum * nCnt = Loop Counter * nSum = running total * Do some error checking. * Complete checking would also make sure each array element * summed is numeric. IF TYPE("nStart") <> "N" OR nStart < 1 nStart = 1 ENDIF IF TYPE("nNum") <> "N" OR nStart + nNum - 1 > ALEN(aInput) nNum = ALEN(aInput) - nStart + 1 ENDIF nSum = 0 FOR nCnt = nStart TO nStart + nNum - 1 nSum = nSum + aInput[nCnt] ENDFOR RETURN nSumIf you have a numeric array called MyArray and want to total the third through eighth elements, you'd issue this call:
? ArraySum(@MyArray,3,6)Now suppose you need a function that totals each row of a two-dimensional array. You could write all the summing code again or you could take advantage of the code you've already written. The following function calls on ArraySum to total each row and just stores the result. It takes advantage of the fact that FoxPro represents all arrays internally as one-dimensional. (See "Hip, Hip, Array" in "DBF, FPT, CDX, DBC—Hike!" for more on that subject.) For good measure, the function returns the number of rows.
* SumRows.PRG * Sum each row of an array, returning the results * in a one-column array. LPARAMETERS aInput, aOutput * aInput = Array with rows to be summed * aOutput = One-dimensional array of sums LOCAL nCnt, nRowCnt, nColCnt * nCnt = loop counter * nRowCnt = number of rows in input * nColCnt = number of columns in input nRowCnt = ALEN(aInput, 1) nColCnt = ALEN(aInput, 2) * Dimension aOutput appropriately DIMENSION aOutput(nRowCnt) FOR nCnt = 1 TO nRowCnt aOutput[nCnt] = ArraySum(@aInput, nColCnt * (nCnt - 1) + 1, nColCnt) ENDFOR RETURN nRowCntIf you have a two-dimensional array called My2DArray, you'd get the row totals like this:
DIMENSION MyTotals[1] = SumRows(@My2DArray, @MyTotals)The key point here is that, in writing SumRows, we didn't have to worry about what variables ArraySum used. Since SumRows makes all its variables local, no routine that it calls can damage its environment. Think of local variables as "a piece of the rock" for your routines. You create local variables with the LOCAL keyword. Local variables are released when you complete the routine in which they were created. You can create local arrays, as well as scalars, using LOCAL. Newly created local variables have logical type and a value of .F.You may have noticed the LPARAMETERS declaration in those two functions. LPARAMETERS is also an addition in Visual FoxPro. It creates formal parameters as local variables, protecting them in the same way that LOCAL protects variables. We recommend all procedures and functions use local variables and local parameters for all internal tasks. Use private variables only when you explicitly want lower-level routines to have access to those variables—for example, when SCATTERing fields for a form (something you don't really need to do in VFP).