So far we have discussed identifiers and only briefly touched on what they actually represent. We know that identifiers are used for constants, variables, program names, unit names, type names, etc. But what do they mean and what happens when two different things are given the same identifier? The identifier for the program name is essentially meaningless. It's origin is from the early days of Pascal (the 70's) and it has been maintained largely for historical reasons. Some compilers do not even complain if the line "program ProgramName;" is omitted all together. Unit identifiers are obviously important because they tell the compiler where to look for groups of identifiers (variables, constants, subroutines). The process of matching an identifier with its actual meaning in the code is known as resolving an identifier. Thus, we could have a variable called SomeVar in a unit called Unit1:
unit Unit1;
interface
var
SomeVar : Integer = 10;
implementation
end.
We could have a different variable called SomeVar in a unit called Unit2. Since these identifiers are in separate units their names do not conflict. Note that in each unit they have a different type and different values.
unit Unit2;
interface
var
SomeVar : String = 'This is a string';
implementation
end.
If we declare Unit1 in the uses clause of the program, then the compiler resolves SomeVar as the variable in Unit1. If we declare Unit2 then the compiler resolves it from Unit2. But what happens if we declare Unit1 and Unit2 in the uses clause simultaneously? The compiler solves this potential conflict by simply taking SomeVar from whichever unit is declared first in the uses clause. If we want to use SomeVar from the unit declared second, Object Pascal provides a syntax to support this need that is similar to the dot "." syntax used by records:
program Example;
uses Unit2, Unit1;
begin
Writeln(SomeVar); // Output: This is a string
Writeln(Unit1.SomeVar); // Output: 10
Writeln(Unit2.SomeVar); // Output: This is a string
end.
This is what happens if we reverse the order of the units in the uses clause:
program Workspace;
uses Unit1, Unit2;
begin
Writeln(SomeVar); // Output: 10
Writeln(Unit1.SomeVar); // Output: 10
Writeln(Unit2.SomeVar); // Output: This is a string
end.
The portion of code where an identifier can be properly resolved is known as its scope. We have mentioned global variables in a program and local variables in subroutines. Global variables are said to have global scope and local variables have local scope. Variables with global scope can be resolved from any code written after they are declared. Consider the following example:
program Workspace;
procedure ExampleProc1;
begin
X := 8; // This throws a compiler error because the identifier is undeclared
// To fix this problem, move this procedure below the declaration of X
end;
var
X : Integer; // X is global from here on out
procedure ExampleProc2;
begin
X := 5; // This is valid. 5 is assigned to the global variable X
end;
procedure ExampleProc3;
var
X : Integer;
begin
X := 10; // The global X is hidden by the local X here.
// This assignment has no effect on the value
// in the global X.
end;
begin
ExampleProc1;
ExampleProc2;
ExampleProc3;
Writeln(X); // What's the value of X here?
Readln;
end.
Where it is located right now, ExampleProc1 is invalid because it references the global variable X before it has been declared. ExampleProc2 is valid and it assigns a value of 5 to the global variable X which persists after this subroutine terminates. ExampleProc3 is also valid, but X has been declared as a local variable here. This means that the global X is invisible to the code in ExampleProc3's begin-end block. Although a value of 10 is assigned to the local X, this variable disappears when the procedure exits (because it is local). This means that the program ultimately writes 5 to the screen because the global X is referenced in the main program block.
Excercise 3-16. Solve the following problems.
Without running the program. Determine what outputs are written to the screen.
25
5
5
25
The first line is 25 because the return value of ExampleProc1 is 5*5. While local X inside of ExampleProc1 is set to 25, global X remains 5. For this reason, the second line is 5. The third line is 5 because ExampleProc2 simply returns the current value of global X. The fourth line is 25 because as a side effect of ExampleProc2, global X was set to 25.
Without running the program. Determine what outputs are written to the screen.
25
5
25
25
Everything is identical to the previous problem except that the argument passed to ExampleProc2 is passed by reference using the var reserved word. That means that instead of simply passing the value of global X to the procedure, a reference to global X is passed. This essentially means that Y becomes global X for the duration of this procedure call.
As we have seen, global variables also exist in units. If a unit global variable is declared in the interface section, it is visible to all code below its declaration within that unit and to all code that uses that unit. If a unit global variable is declared in the implementation section, it is only visible to the code below its declaration within that unit. This difference in visibility between the interface and implementation sections allows us to carefully design the interface and prevent access to more internal aspects of a unit that are not meant to be used directly by other code. This is analogous to driving a car in which the interface to the wheels is through a power steering wheel. The driver does not get direct access to the components that actually turn the wheels (and for good reason). This is again an example of encapsulation.
Finally, we should point out that scope does not just apply at a global and local level. In Object Pascal, scope can be changed for just a few lines of code by using the with statement:
program Example;
type
TRec = class
Int : Integer;
Str : String;
end;
var
Rec : TRec;
begin
Rec.Int := 10;
Rec.Str := 'test';
// This code performs the identical function as the code above
with Rec do
begin
Int := 10;
Str := 'test';
end;
end.
The with statement here causes the compiler to temporarily add the fields of TRec to the local scope. This allows Int and Str to be resolved as if they were regular variables. Although this example is trivial, the with statement can be extremely useful in the case of records with a large number of fields or when such fields are used in complex expressions.