Chapter 1. Delphi Pascal
Delphi Pascal is an object-oriented extension of traditional Pascal. It is not quite a proper superset of ISO standard Pascal, but if you remember Pascal from your school days, you will easily pick up Delphi’s extensions. Delphi is not just a fancy Pascal, though. Delphi adds powerful object-oriented features, without making the language too complicated. Delphi has classes and objects, exception handling, multithreaded programming, modular programming, dynamic and static linking, OLE automation, and much, much more.
This chapter describes Delphi’s extensions to Pascal. You should already be familiar with traditional Pascal or one of the other popular extensions to Pascal, such as Object Pascal. If you already know Borland’s Object Pascal from the Turbo Pascal products, you will need to learn a new object model (detailed in Chapter 2 ), plus other new features.
Borland uses the name “Object Pascal” to refer to Delphi’s programming language, but many other languages use the same name, which results in confusion. This book uses the name “Delphi Pascal” to refer to the Delphi programming language, leaving Object Pascal for the many other object-oriented variations of Pascal.
Units
Delphi Pascal is a modular programming language, and the basic module is called a unit. To compile and link a Delphi program, you need a program source file and any number of additional units in source or object form. The program source file is usually called a project source file because the project can be a program or a library—that is, a dynamically linked library (DLL).
When Delphi links a program or library, it can statically link all the units into a single .exe or .dll file, or it can dynamically link to units that are in packages. A package is a special kind of DLL that contains one or more units and some extra logic that enables Delphi to hide the differences between a statically linked unit and a dynamically linked unit in a package. See the section Packages,” later in this chapter, for more information about packages.
Forms and Files
Some units represent forms. A form is Delphi’s term for a window you can edit with Delphi’s GUI builder. A form description is stored in a .dfm file, which contains the form’s layout, contents, and properties.
Every .dfm file has an associated .pas file, which contains the code for that form. Forms and .dfm files are part of Delphi’s integrated development environment (IDE), but are not part of the formal Delphi Pascal language. Nonetheless, the language includes several features that exist solely to support Delphi’s IDE and form descriptions. Read about these features in depth in Chapter 3.
Tip
A binary .dfm file is actually a 16-bit .res (Windows resource) file, which maintains compatibility with the first version of Delphi. Versions 2 and later produce only 32-bit programs, so Delphi’s linker converts the .dfm resource to a 32-bit resource automatically. Thus, binary .dfm files are usually compatible with all versions of Delphi. Delphi 5 also supports textual .dfm files. These files are plain text and are not compatible with prior versions of Delphi, at least not without conversion back to the binary format. The only way to tell whether a .dfm file is binary or text is to open the file and check the contents. An easy way to do this programmatically is to test the first three bytes, which are always $FF $0A $00 in a binary .dfm file.
Table 1-1 briefly describes the files you are likely to find in Delphi and what they are used for. Files marked with “(IDE)” are not part of Delphi Pascal, but are used by the IDE.
Extension |
Description |
.bpg |
Project group (IDE) |
.bpl |
Compiled package (special kind of DLL) |
.cfg |
Options for the command line compiler |
.dcp |
Compiled package information, needed to link with a package |
.dcr |
Component bitmap resource (IDE) |
.dcu |
Unit object code |
.dfm |
Form description (IDE) |
.dof |
Project options file (IDE) |
.dpk |
Source file for building a package |
.dpr |
Main source file for a program or library |
.drc |
Resource script for |
.dsk |
Desktop layout (IDE) |
.pas |
Unit source code |
.res |
Windows resource (every .dpr has an associated .res file) |
Separating Interface from Implementation
A unit has two parts: interface and implementation. The interface part declares the types, variables, constants, and routines that are visible to other units. The implementation section provides the guts of the routines declared in the interface section. The implementation section can have additional declarations that are private to the unit’s implementation. Thus, units are Delphi’s primary means of information hiding.
One unit can use another unit, that is, import the declarations from that other unit. A change to a unit’s interface requires a recompilation of all units that use the changed declaration in the modified unit. Delphi’s compiler manages this automatically, so you don’t need to use makefiles to compile Delphi projects.
You can use a unit in the interface or implementation section, and the choice is important when building a project:
If unit A uses unit B in its interface section, changes to unit B’s interface are propagated as changes to unit A’s interface. Delphi must recompile all the units that use unit A.
If unit A uses unit B in its implementation section, only unit A must be recompiled to use the new declarations in unit B.
Units cannot have circular references in their interface sections. Sometimes, you will run into two class declarations that contain mutually dependent declarations. The simplest solution is to use a single unit, but if you have reasons to declare the classes in separate units, you can use an abstract base class in one or both units to eliminate the circular dependency. (See Chapter 2 for more information.)
Initializing and Finalizing
Every unit can have an initialization and a finalization section. Code in every initialization section runs before the program or library’s main begin-end block. Code in the finalization section runs after the program terminates or when the library is unloaded. Delphi runs the initialization sections using a depth-first traversal of the unit dependency tree. In other words, before a unit’s initialization code runs, Delphi runs the initialization section of every unit it uses. A unit is initialized only once. Example 1-1 demonstrates how Delphi initializes and finalizes units.
program Example1_1; uses unitA; {$AppType Console} begin WriteLn('Example 1-1 main program'); end. unit unitA; interface uses unitB; implementation initialization WriteLn('unitA initialization'); finalization WriteLn('unitA finalization'); end. unit unitB; interface implementation initialization WriteLn('unitB initialization'); finalization WriteLn('unitB finalization'); end.
When you compile and run Example 1-1, be sure to run it from a command prompt, not the IDE, or else the console will appear and disappear before you can see the output, which is shown as Example 1-2.
The System and SysInit Units
The System
and SysInit
units
are automatically included in every unit, so all of the declarations
in these units are effectively part of the Delphi Pascal language,
and the compiler has special knowledge about many of the functions
and procedures in the System
and
SysInit
units. Chapter 5, is a
complete reference to the system routines and declarations meant for
your use.
Programs
A
Delphi program looks similar to a traditional Pascal program,
starting with the program
keyword and using a
begin
-end
block for the main
program. Delphi programs are usually short, though, because the real
work takes place in one or more separate units. In a GUI application,
for example, the main program usually calls an initialization
procedure, creates one or more forms (windows), and calls a procedure
for the Windows event loop.
For compatibility with standard Pascal, Delphi allows a parameter list after the program name, but—like most modern Pascal compilers—it ignores the identifiers listed there.
In
a GUI application, you cannot use the standard Pascal I/O procedures
because there is no input device to read from and no output device to
write to. Instead, you can compile a console application, which can read and write using standard
Pascal I/O routines. (See Chapter 8, to learn
about the $AppType
directive, which tells Delphi
to build a console or a GUI application.)
A program’s
uses
declaration lists the units that make up the
program. Each unit name can be followed by an in
directive that specifies a filename. The IDE and compiler use the
filename to locate the units that make up the project. Units without
an in
directive are usually library units, and are
not part of the project’s source code. If a unit has an
associated form, the IDE also stores the form name in a comment.
Example 1-3 shows a typical program source file.
program Typical; uses Forms, Main in 'Main.pas' {MainForm}, MoreStuff in 'MoreStuff.pas' {Form2}, Utils in 'Utils.pas'; {$R *.RES} begin Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.CreateForm(TForm2, Form2); Application.Run; end.
The Forms
unit is part of the standard Delphi
library, so it does not have an in
directive and
source file. The other units have source filenames, so Delphi’s
IDE manages those files as part of the project. To learn about the
$R
compiler directive, see Chapter 8. The Application
object is
part of Delphi’s visual component library and is not covered in
this book. Consult Delphi’s online help for information about
the Application
object and the rest of the
VCL.
Libraries
A
Delphi library compiles to a standard Windows DLL. A library source
file looks the same as a program source file, except that it uses the
library
keyword instead of
program
. A library typically has an
exports
declaration, which lists the routines that
the DLL exports. The exports
declaration is
optional, and if you intend to use a unit in a library, it’s
usually best to put the exports
declaration in the
unit, close to the subroutine you are exporting. If you don’t
use the unit in a library, the exports
declaration
has no impact.
The main
body of the library—its
begin
-end
block—executes
each time the library is loaded into an application. Thus, you
don’t need to write a DLL procedure to handle the
DLL_PROCESS_ATTACH
event. For process detach and
thread events, though, you must write a handler. Assign the handler
to the DllProc
variable. Delphi takes care of
registering the procedure with Windows, and Windows calls the
procedure when a process detaches or when a thread attaches or
detaches. Example 1-4 shows a simple DLL procedure.
library Attacher; uses Windows; procedure Log(const Msg: string); begin MessageBox(0, PChar(Msg), 'Attacher', Mb_IconInformation + Mb_OK); end; procedure AttachDetachProc(Reason: Integer); begin case Reason of Dll_Process_Detach: Log('Detach Process'); Dll_Thread_Attach: Log('Attach Thread'); Dll_Thread_Detach: Log('Detach Thread'); else Log('Unknown reason!'); end; end; begin // This code runs each time the DLL is loaded into a new process. Log('Attach Process'); DllProc := @AttachDetachProc; end.
Using Dynamic Memory
When using a DLL, you must be careful about dynamic memory. Any
memory allocated by a DLL is freed when the DLL is unloaded. Your
application might retain pointers to that memory, though, which can
cause access violations or worse problems if you aren’t
careful. The simplest solution is to use the
ShareMem
unit as the first unit in your
application and in every library the application loads. The
ShareMem
unit redirects all memory requests to a
single DLL (BorlndMM.dll ), which is loaded as
long as the application is running. You can load and unload DLLs
without worrying about dangling
pointers.
Sharing Objects
ShareMem
solves one kind of memory problem, but
not another: class identity. If class A
is used in
the application and in a DLL, Delphi cannot tell that both modules
use the same class. Although both modules use the same class name,
this doesn’t mean the classes are identical. Delphi takes the
safest course and assumes the classes are different; if you know
better, you have no easy way to inform
Delphi.
Sometimes, having separate class identities does not cause any
problems, but if your program tries to use an object reference across
a DLL boundary, the is
and as
operators will not work the way you expect them to. Because the DLL
thinks class A
is different from the
application’s class A
, the
is
operator always returns
False.
One way to circumvent this problem is not to pass objects across DLL
boundaries. If you have a graphic object, for example, don’t
pass a TBitmap
object, but pass a Windows handle
(HBITMAP
) instead. Another solution is to use
packages. Delphi automatically manages the class identities in
packages to avoid this problem.
Setting the Image Base
When you create a library, be sure to set the Image Base option. Windows must load every module (DLL and application) at a unique image base address. Delphi’s default is $00400000, but Windows uses that address for the application, so it cannot load a DLL at the same address. When Windows must move a DLL to a different address, you incur a performance penalty, because Windows must rewrite a relocation table to reflect the new addresses. You cannot guarantee that every DLL will have a unique address because you cannot control the addresses other DLL authors use, but you can do better than the default. You should at least make sure your DLLs use a different image base than any of the standard Delphi packages and Windows DLLs. Use Windows Quick View to check a file’s image base.
Packages
Delphi can link a unit statically with a program or library, or it can link units dynamically. To link dynamically to one or more units, you must put those units in a package, which is a special kind of DLL. When you write a program or library, you don’t need to worry about how the units will be linked. If you decide to use a package, the units in the package are not linked into your .exe or .dll, but instead, Delphi compiles a reference to the package’s DLL (which has the extension .bpl for Borland Package Library).
Packages avoid the problems of DLLs, namely, managing memory and
class identities. Delphi keeps track of the classes defined in each
unit and makes sure that the application and all associated packages
use the same class identity for the same class, so the
is
and as
operators work
correctly.
Design-Time Versus Runtime
Delphi’s IDE uses packages to load components, custom forms, and other design-time units, such as property editors. When you write components, keep their design-time code in a design-time package, and put the actual component class in a runtime package. Applications that use your component can link statically with the component’s .dcu file or link dynamically with the runtime package that contains your component. By keeping the design-time code in a separate package, you avoid linking any extraneous code into an application.
Note that the design-time package requires the runtime package because you cannot link one unit into multiple packages. Think of an application or library as a collection of units. You cannot include a unit more than once in a single program—it doesn’t matter whether the units are linked statically or dynamically. Thus, if an application uses two packages, the same unit cannot be contained in both packages. That would be the equivalent of linking the unit twice.
Building a Package
To build a package, you need to create a .dpk file, or package source file. The .dpk file lists the units the package contains, and it also lists the other packages the new package requires. The IDE includes a convenient package editor, or you can edit the .dpk file by hand, using the format shown in Example 1-5.
package Sample; {$R 'COMP.DCR'} {$IMAGEBASE $09400000} {$DESCRIPTION 'Sample Components'} requires vcl50; contains Comp in 'Comp.pas'; end.
As with any DLL, make sure your packages use unique addresses for their Image Base options. The other options are self-explanatory. You can include options as compiler directives in the .dpk file (as explained in Chapter 8), or you can let the package editor in the IDE write the options for you.
Data Types
Delphi Pascal supports several extensions to the standard Pascal data types. Like any Pascal language, Delphi supports enumerations, sets, arrays, integer and enumerated subranges, records, and variant records. If you are accustomed to C or C++, make sure you understand these standard Pascal types, because they can save you time and headache. The differences include the following:
Integer Types
The
basic integer type is Integer
. The
Integer
type represents the natural size of an
integer, given the operating system and platform. Currently,
Integer
represents a 32-bit integer, but you must
not rely on that. The future undoubtedly holds a 64-bit operating
system running on 64-bit hardware, and calling for a 64-bit
Integer
type. To help cope with future changes,
Delphi defines some types whose size depends on the natural integer
size and other types whose sizes are fixed for all future versions of
Delphi. Table 1-2 lists the standard integer
types. The types marked with natural size might
change in future versions of Delphi, which means the range will also
change. The other types will always have the size and range shown.
Real Types
Delphi
has several floating-point types. The basic types are
Single
, Double
, and
Extended
. Single
and
Double
correspond to the standard sizes for the
IEEE-754 standard, which is the basis for floating-point hardware on
Intel platforms and in Windows. Extended
is the
Intel extended precision format, which conforms to the minimum
requirements of the IEEE-754 standard for extended double precision.
Delphi defines the standard Pascal Real
type as a
synonym for Double
. See the descriptions of each
type in Chapter 5 for details about
representation.
Tip
The
floating-point hardware uses the full precision of the
Extended
type for its computations, but that
doesn’t mean you should use Extended
to
store numbers. Extended
takes up 10 bytes, but the
Double
type is only 8 bytes and is more efficient
to move into and out of the floating-point unit. In most cases, you
will get better performance and adequate precision by using
Double
.
Errors in floating-point arithmetic,
such as dividing by zero, result in runtime errors. Most Delphi
applications use the SysUtils
unit, which maps
runtime errors into exceptions, so you will usually receive a
floating-point exception for such errors. Read more about exceptions
and errors in Exception Handling,” later in this
chapter.
The floating-point types also have representations for infinity and not-a-number (NaN). These special values don’t arise normally unless you set the floating-point control word. You can read more about infinity and NaN in the IEEE-754 standard, which is available for purchase from the IEEE. Read about the floating-point control word in Intel’s architecture manuals, especially the Pentium Developer’s Manual, volume 3, Architecture and Programming Manual. Intel’s manuals are available online at http://developer.intel.com/design/processor/.
Delphi also
has a fixed-point type, Currency
. This type
represents numbers with four decimal places in the range
-922,337,203,685,477.5808 to 922,337,203,685,477.5807, which is
enough to store the gross income for the entire planet, accurate to a
hundredth of a cent. The Currency
type employs the
floating-point processor, using 64 bits of precision in two’s
complement form. Because Currency
is a
floating-point type, you cannot use any integer operators (such as
bit shifting or masking).
Warning
The floating-point unit (FPU) can perform calculations in
single-precision, double-precision, or extended-precision mode.
Delphi sets the FPU to extended precision, which provides full
support for the Extended
and
Currency
types. Some Windows API functions,
however, change the FPU to double precision. At double precision, the
FPU maintains only 53 bits of precision instead of
64.
When
the FPU uses double precision, you have no reason to use
Extended
values, which is another reason to use
Double
for most computations. A bigger problem is
the Currency
type. You can try to track down
exactly which functions change the FPU control word and reset the
precision to extended precision after the errant functions return.
(See the Set8087CW
function in Chapter 5.) Another solution is to use the
Int64
type instead of Currency
,
and implement your own fixed-point scaling in the manner shown in
Example 1-6.
resourcestring sInvalidCurrency = 'Invalid Currency string: ''%s'''; const Currency64Decimals = 4; // number of fixed decimal places Currency64Scale = 10000; // 10**Decimal64Decimals type Currency64 = type Int64; function Currency64ToString(Value: Currency64): string; begin Result := Format('%d%s%.4d', [Value div Currency64Scale, DecimalSeparator, Abs(Value mod Currency64Scale)]); end; function StringToCurrency64(const Str: string): Currency64; var Code: Integer; Fraction: Integer; FractionString: string[Currency64Decimals]; I: Integer; begin // Convert the integer part and scale by Currency64Scale Val(Str, Result, Code); Result := Result * Currency64Scale; if Code = 0 then // integer part only in Str Exit else if Str[Code] = DecimalSeparator then begin // The user might specify more or fewer than 4 decimal points, // but at most 4 places are meaningful. FractionString := Copy(Str, Code+1, Currency64Decimals); // Pad missing digits with zeros. for I := Length(FractionString)+1 to Currency64Decimals do FractionString[I] := '0'; SetLength(FractionString, Currency64Decimals); // Convert the fractional part and add it to the result. Val(FractionString, Fraction, Code); if Code = 0 then begin if Result < 0 then Result := Result - Fraction else Result := Result + Fraction; Exit; end; end; // The string is not a valid currency string (signed, fixed point // number). raise EConvertError.CreateFmt(sInvalidCurrency, [Str]); end;
Arrays
In additional to standard Pascal arrays, Delphi defines several extensions for use in special circumstances. Dynamic arrays are arrays whose size can change at run-time. Open arrays are array parameters that can accept any size array as actual arguments. A special case of open arrays lets you pass an array of heterogeneous types as an argument to a routine. Delphi does not support conformant arrays, as found in ISO standard Pascal, but open arrays offer the same functionality.
Dynamic arrays
A dynamic array is an array whose size is determined at runtime. You
can make a dynamic array grow or shrink while the program runs.
Declare a dynamic array without an index type. The index is always an
integer, and always starts at zero. At runtime you can change the
size of a dynamic array with the SetLength
procedure. Assignment of a dynamic array assigns a reference to the
same array. Unlike strings, dynamic arrays do not use copy-on-write,
so changing an element of a dynamic array affects all references to
that array. Delphi manages dynamic arrays using reference counting so
when an array goes out of scope, its memory is automatically freed.
Example 1-7 shows how to declare and use a dynamic
array.
var I: Integer; Data: array of Double; // Dynamic array storing Double values F: TextFile; // Read data from this file Value: Double; begin AssignFile(F, 'Stuff.dat'); Reset(F); while not Eof(F) do begin ReadLn(F, Value); // Inefficient, but simple way to grow a dynamic array. In a real // program, you should increase the array size in larger chunks, // not one element at a time. SetLength(Data, Length(Data) + 1); Data[High(Data)] := Value; end; CloseFile(F); end;
Warning
Delphi
checks array indices to make sure they are in bounds. (Assuming you
have not disabled range checks; see the $R
directive in Chapter 8.) Empty dynamic arrays are
an exception. Delphi represents an empty dynamic array as a
nil
pointer. If you attempt to access an element
of an empty dynamic array, Delphi dereferences the
nil
pointer, resulting in an access violation, not
a range check error.
Open arrays
You can
declare a parameter to a function or procedure as an open array. When calling the routine, you can pass any size
array (with the same base type) as an argument. The routine should
use the Low
and High
functions
to determine the bounds of the array. (Delphi always uses zero as the
lower bound, but the Low
and
High
functions tell the maintainer of your code
exactly what the code is doing. Hard-coding 0
is
less clear.) Be sure to declare the parameter as
const
if the routine does not need to modify the
array, or as var
if the routine modifies the array
contents.
The declaration for an open array argument looks like the declaration for a dynamic array, which can cause some confusion. When used as a parameter, an array declaration without an index type is an open array. When used to declare a local or global variable, a field in a class, or a new type, an array declaration without an index means a dynamic array.
You can pass a dynamic array to a routine that declares its argument as an open array, and the routine can access the elements of the dynamic array, but cannot change the array’s size. Because open arrays and dynamic arrays are declared identically, the only way to declare a parameter as a dynamic array is to declare a new type identifier for the dynamic array type, as shown below:
procedure CantGrow(var Data: array of integer); begin // Data is an open array, so it cannot change size. end; type TArrayOfInteger = array of integer; // dynamic array type procedure Grow(var Data: TArrayOfInteger); begin // Data is a dynamic array, so it can change size. SetLength(Data, Length(Data) + 1); end;
You can pass a dynamic array to the CantGrow
procedure, but the array is passed as an open array, not as a dynamic
array. The procedure can access or change the elements of the array,
but it cannot change the size of the array.
If you must call a Delphi function from another language, you can pass an open array argument as a pointer to the first element of the array and the array length minus one as a separate 32-bit integer argument. In other words, the lower bound for the array index is always zero, and the second parameter is the upper bound.
You can also create an open array argument by enclosing a series of
values in square brackets. The open array expression can be used only
as an open array argument, so you cannot assign such a value to an
array-type variable. You cannot use this construct for a
var
open array. Creating an open array on the fly
is a convenient shortcut, avoiding the need to declare a
const
array:
Avg := ComputeAverage([1, 5, 7, 42, 10, -13]);
The
Slice
function is another way to pass an array to
a function or procedure. Slice
lets you pass part
of an array to a routine. Chapter 5 describes
Slice
in detail.
Type variant open arrays
Another
kind of open array parameter is the type variant open array, or array of const
. A variant
open array lets you pass a heterogeneous array, that is, an array
where each element of the array can have a different type. For each
array element, Delphi creates a TVarRec
record,
which stores the element’s type and value. The array of
TVarRec
records is passed to the routine as a
const
open array. The routine can examine the type
of each element of the array by checking the VType
member of each TVarRec
record. Type variant open
arrays give you a way to pass a variable size argument list to a
routine in a type-safe manner.
TVarRec
is a variant record similar to a
Variant
, but implemented differently. Unlike a
Variant
, you can pass an object reference using
TVarRec
. Chapter 6, lists all
the types that TVarRec
supports. Example 1-8 shows a simple example of a routine that
converts a type variant open array to a string.
function AsString(const Args: array of const): string; var I: Integer; S: String; begin Result := ''; for I := Low(Args) to High(Args) do begin case Args[I].VType of vtAnsiString: S := PChar(Args[I].VAnsiString); vtBoolean: if Args[I].VBoolean then S := 'True' else S := 'False'; vtChar: S := Args[I].VChar; vtClass: S := Args[I].VClass.ClassName; vtCurrency: S := FloatToStr(Args[I].VCurrency^); vtExtended: S := FloatToStr(Args[I].VExtended^); vtInt64: S := IntToStr(Args[I].VInt64^); vtInteger: S := IntToStr(Args[I].VInteger); vtInterface: S := Format('%p', [Args[I].VInterface]); vtObject: S := Args[I].VObject.ClassName; vtPChar: S := Args[I].VPChar; vtPointer: S := Format('%p', [Args[I].VPointer]); vtPWideChar: S := Args[I].VPWideChar; vtString: S := Args[I].VString^; vtVariant: S := Args[I].VVariant^; vtWideChar: S := Args[I].VWideChar; vtWideString: S := WideString(Args[I].VWideString); else raise Exception.CreateFmt('Unsupported VType=%d', [Args[I].VType]); end; Result := Result + S; end; end;
Strings
Delphi has four kinds of strings: short, long, wide, and zero-terminated. A short string is a counted array of characters, with up to 255 characters in the string. Short strings are not used much in Delphi programs, but if you know a string will have fewer than 255 characters, short strings incur less overhead than long strings.
Long strings can be any size, and the size can change at runtime. Delphi uses a copy-on-write system to minimize copying when you pass strings as arguments to routines or assign them to variables. Delphi maintains a reference count to free the memory for a string automatically when the string is no longer used.
Wide strings are also dynamically allocated and managed, but they do
not use reference counting. When you assign a wide string to a
WideString
variable, Delphi copies the entire
string.
Warning
Delphi checks string references the same way it checks dynamic array
references, that is, Delphi checks subscripts to see if they are in
range, but an empty long or wide string is represented by a
nil
pointer. Testing the bounds of an empty long
or wide string, therefore, results in an access violation instead of
a range check
error.
A zero-terminated string is an array of characters, indexed by an integer starting from zero. The string does not store a size, but uses a zero-valued character to mark the end of the string. The Windows API uses zero-terminated strings, but you should not use them for other purposes. Without an explicit size, you lose the benefit of bounds checking, and performance suffers because some operations require two passes over the string contents or must process the string contents more slowly, always checking for the terminating zero value. Delphi will also treat a pointer to such an array as a string.
For your convenience, Delphi stores a zero value at the end of long
and wide strings, so you can easily cast a long string to the type
PAnsiChar
, PChar
, or
PWideChar
to obtain a pointer to a zero-terminated
string. Delphi’s PChar
type is the
equivalent of char*
in C or
C++.
String literals
You can
write a string literal in the standard Pascal way, or use a pound
sign (#
) followed by an integer to specify a
character by value, or use a caret (^
) followed by
a letter to specify a control character. You can mix any kind of
string to form a single literal, for example:
'Normal string: '#13#10'Next line (after CR-LF)'^I'That was a ''TAB'''
The
caret (^
) character toggles the sixth bit ($40) of
the character’s value, which changes an upper case letter to
its control character equivalent. If the character is lowercase, the
caret clears the fifth and sixth bits ($60). This means you can apply
the caret to nonalphabetic characters. For example,
^2
is the same as 'r'
because
'2'
has the ordinal value $32, and toggling the
$40 bit makes it $72, which is the ordinal value for
'r'
. Delphi applies the same rules to every
character, so you can use the caret before a space, tab, or return,
with the result that your code will be completely unreadable.
Mixing string types
You can freely mix all different kinds
of strings, and Delphi does its best to make sense out of what you
are trying to do. You can concatenate different kinds of strings, and
Delphi will narrow a wide string or widen a narrow string as needed.
To pass a string to a function that expects a
PChar
parameter, just cast a long string to
PChar
. A short string does not automatically have
a zero byte at the end, so you need to make a temporary copy, append
a #0
byte, and take the address of the first
character to get a PChar
value.
Unicode and multibyte strings
Delphi supports
Unicode with its WideChar
,
WideString
and PWideChar
types.
All the usual string operations work for wide strings and narrow
(long or short) strings. You can assign a narrow string to a
WideString
variable, and Delphi automatically
converts the string to Unicode. When you assign a wide string to a
long (narrow) string, Delphi uses the ANSI code page to map Unicode
characters to multibyte characters.
A multibyte string is a string where a single character might occupy more than one byte. (The Windows term for a multibyte character set is double-byte character set.) Some national languages (e.g., Japanese and Chinese) use character sets that are much larger than the 256 characters in the ANSI character set. Multibyte character sets use one or two bytes to represent a character, allowing many more characters to be represented. In a multibyte string, a byte can be a single character, a lead byte (that is, the first byte of a multibyte character), or a trailing byte (the second byte of a multibyte character). Whenever you examine a string one character at a time, you should make sure that you test for multibyte characters because the character that looks like, say, the letter “A” might actually be the trailing byte of an entirely different character.
Ironically, some of Delphi’s string handling functions do not
handle multibyte strings correctly. Instead, the
SysUtils
unit has numerous string functions that
work correctly with multibyte strings. Handling multibyte strings is
especially important for filenames, and the
SysUtils
unit has special functions for working
with multibyte characters in filenames. See Appendix B, for details.
Windows NT and Windows 2000 support
narrow and wide versions of most API functions. Delphi defaults to
the narrow versions, but you can call the wide functions just as
easily. For example, you can call CreateFileW
to
create a file with a Unicode filename, or you can call
CreateFileA
to create a file with an ANSI
filename. CreateFile
is the same as
CreateFileA
. Delphi’s VCL uses the narrow
versions of the Windows controls, to maintain compatibility with all
versions of Windows. (Windows 95 and 98 do not support most Unicode
controls.)
Boolean Types
Delphi
has the usual Pascal Boolean
type, but it also has
several other types that make it easier to work with the Windows API.
Numerous API and other functions written in C or C++ return values
that are Boolean in nature, but are documented as returning an
integer. In C and C++, any non-zero value is considered True, so
Delphi defines the LongBool
,
WordBool
, and ByteBool
values
with the same semantics.
For example, if you must call a function that was written in C, and
the function returns a Boolean result as a short integer, you can
declare the function with the WordBool
return type
and call the function as you would any other Boolean-type function in
Pascal:
function SomeCFunc: WordBool; external 'TheCDll.dll'; ... if SomeCFunc then ...
It doesn’t matter what numeric value
SomeCFunc
actually returns; Delphi will treat zero
as False and any other value as True. You can use any of the C-like
logical types the same way you would the native Delphi
Boolean
type. The semantics are identical. For
pure Delphi code, you should always use Boolean
.
Variants
Delphi
supports OLE variant types, which makes it easy to write an OLE
automation client or server. You can use Variant
s
in any other situation where you want a variable whose type can
change at runtime. A Variant
can be an array, a
string, a number, or even an IDispatch
interface.
You can use the Variant
type or the
OleVariant
type. The difference is that an
OleVariant
takes only COM-compatible types, in
particular, all strings are converted to wide strings. Unless the
distinction is important, this book uses the term
Variant
to refer to both
types.
A Variant
variable is always initialized to
Unassigned
. You can assign almost any kind of
value to the variable, and it will keep track of the type and value.
To learn the type of a Variant
, call the
VarType
function. Chapter 6
lists the values that VarType
can return. You can
also access Delphi’s low-level implementation of
Variant
s by casting a Variant
to the TVarData
record type. Chapter 5 describes TVarData
in
detail.
When you use a Variant
in an expression, Delphi
automatically converts the other value in the expression to a
Variant
and returns a Variant
result. You can assign that result to a statically typed variable,
provided the Variant
’s type is compatible
with the destination variable.
The
most common use for Variant
s is to write an OLE
automation client. You can assign an IDispatch
interface to a Variant
variable, and use that
variable to call functions the interface declares. The compiler does
not know about these functions, so the function calls are not checked
for correctness until runtime. For example, you can create an OLE
client to print the version of Microsoft Word installed on your
system, as shown in the following code. Delphi doesn’t know
anything about the Version
property or any other
method or property of the Word OLE client. Instead, Delphi compiles
your property and method references into calls to the
IDispatch
interface. You lose the benefit of
compile-time checks, but you gain the flexibility of runtime binding.
(If you want to keep the benefits of type safety, you will need a
type library from the vendor of the OLE automation server. Use the
IDE’s type library editor to extract the COM interfaces the
server’s type library defines. This is not part of the Delphi
language, so the details are not covered in this
book.)
var WordApp: Variant; begin try WordApp := CreateOleObject('Word.Application'); WriteLn(WordApp.Version); except WriteLn('Word is not installed'); end; end;
Pointers
Pointers are not as important in Delphi as they are in C or C++. Delphi has real arrays, so there is no need to simulate arrays using pointers. Delphi objects use their own syntax, so there is no need to use pointers to refer to objects. Pascal also has true pass-by-reference parameters. The most common use for pointers is interfacing to C and C++ code, including the Windows API.
C and C++ programmers will be glad that Delphi’s rules for
using pointers are more C-like than Pascal-like. In particular, type
checking is considerably looser for pointers than for other types.
(But see the $T
and
$TypedAddress
directives, in Chapter 8, which tighten up the loose rules.)
The type
Pointer
is a generic pointer type, equivalent to
void*
in C or C++. When you assign a pointer to a
variable of type Pointer
, or assign a
Pointer
-type expression to a pointer variable, you
do not need to use a type cast. To take the address of a variable or
routine, use Addr
or @
(equivalent to &
in C or C++). When using a
pointer to access an element of a record or array, you can omit the
dereference operator (^
). Delphi can tell that the
reference uses a pointer, and supplies the ^
operator automatically.
You can
perform arithmetic on pointers in a slightly more restricted manner
than you can in C or C++. Use the Inc
or
Dec
statements to advance or retreat a pointer
value by a certain number of base type elements. The actual pointer
value changes according to the size of the pointer’s base type.
For example, incrementing a pointer to an Integer
advances the pointer by 4 bytes:
var IntPtr: ^Integer; begin ... Inc(IntPtr); // Make IntPtr point to the next Integer, 4 bytes later Inc(IntPtr, 3); // Increase IntPtr by 12 bytes = 3 * SizeOf(Integer)
Programs that interface directly with the Windows API often need to
work with pointers explicitly. For example, if you need to create a
logical palette, the type definition of
TLogPalette
requires dynamic memory allocation and
pointer manipulation, using a common C hack of declaring an array of
one element. In order to use TLogPalette
in
Delphi, you have to write your Delphi code using C-like style, as
shown in Example 1-9.
// Create a gray-scale palette with NumColors entries in it. type TNumColors = 1..256; function MakeGrayPalette(NumColors: TNumColors): HPalette; var Palette: PLogPalette; // pointer to a TLogPalette record I: TNumColors; Gray: Byte; begin // TLogPalette has a palette array of one element. To allocate // memory for the entire palette, add the size of NumColors-1 // palette entries. GetMem(Palette, SizeOf(TLogPalette) + (NumColors-1)*SizeOf(TPaletteEntry)); try // In standard Pascal, you must write Palette^.palVersion, // but Delphi dereferences the pointer automatically. Palette.palVersion := $300; Palette.palNumEntries := NumColors; for I := 1 to NumColors do begin // Use a linear scale for simplicity, even though a logarithmic // scale gives better results. Gray := I * 255 div NumColors; // Turn off range checking to access palette entries past the first. {$R-} Palette.palPalEntry[I-1].peRed := Gray; Palette.palPalEntry[I-1].peGreen := Gray; Palette.palPalEntry[I-1].peBlue := Gray; Palette.palPalEntry[I-1].peFlags := 0; {$R+} end; // Delphi does not dereference pointers automatically when used // alone, as in the following case: Result := CreatePalette(Palette^); finally FreeMem(Palette); end; end;
Function and Method Pointers
Delphi lets you take the address of a function, procedure, or method, and use that address to call the routine. For the sake of simplicity, all three kinds of pointers are called procedure pointers.
A procedure pointer has a type that specifies a function’s return type, the arguments, and whether the pointer is a method pointer or a plain procedure pointer. Source code is easier to read if you declare a procedure type and then declare a variable of that type, for example:
type TProcedureType = procedure(Arg: Integer); TFunctionType = function(Arg: Integer): string; var Proc: TProcedureType; Func: TFunctionType; begin Proc := SomeProcedure; Proc(42); // Call Proc as though it were an ordinary procedure
Usually, you can assign a procedure to a procedure variable directly. Delphi can tell from context that you are not calling the procedure, but are assigning its address. (A strange consequence of this simple rule is that a function of no arguments whose return type is a function cannot be called in the usual Pascal manner. Without any arguments, Delphi thinks you are trying to take the function’s address. Instead, call the function with empty parentheses—the same way C calls functions with no arguments.)
You can also use the @
or Addr
operators to get the address of a routine. The explicit use of
@
or Addr
provides a clue to
the person who must read and maintain your software.
Use a nil
pointer for procedure pointers the same
way you would for any other pointer. A common way to test a procedure
variable for a nil
pointer is with the
Assigned
function:
if Assigned(Proc) then Proc(42);
Type Declarations
Delphi follows the basic rules of
type compatibility that ordinary Pascal follows for arithmetic,
parameter passing, and so on. Type declarations have one new trick,
though, to support the IDE. If a type declaration begins with the
type
keyword, Delphi creates separate runtime type
information for that type, and treats the new type as a distinct type
for var
and out
parameters. If
the type declaration is just a synonym for another type, Delphi does
not ordinarily create separate RTTI for the type synonym. With the
extra type
keyword, though, separate RTTI tables
let the IDE distinguish between the two types. You can read more
about RTTI in Chapter 3.
Variables and Constants
Unlike standard Pascal, Delphi lets you declare the type of a constant, and you can initialize a global variable to a constant value. Delphi also supports multithreaded applications by letting you declare variables that have distinct values in each thread of your application.
Typed Constants
When you declare the type of a constant, Delphi sets aside memory for that constant and treats it as a variable. You can assign a new value to the “constant,” and it keeps that value. In C and C++, this entity is called a static variable.
// Return a unique number each time the function is called. function Counter: Integer; const Count: Integer = 0; begin Inc(Count); Result := Count; end;
At the unit level, a variable retains its value in the same way, so you can declare it as a constant or as a variable. Another way to write the same function is as follows:
var Count: Integer = 0; function Counter: Integer; begin Inc(Count); Result := Count; end;
The term “typed constant” is clearly a misnomer, and at
the unit level, you should always use an initialized
var
declaration instead of a typed constant. You
can force yourself to follow this good habit by disabling the
$J
or $WriteableConst
compiler
directive, which tells Delphi to treat all constants as constants.
The default, however, is to maintain backward compatibility and let
you change the value of a typed constant. See Chapter 8 for more information about these compiler
directives.
For local variables in a procedure or function, you cannot initialize variables, and typed constants are the only way to keep values that persist across different calls to the routine. You need to decide which is worse: using a typed constant or declaring the persistent variable at the unit level.
Thread Variables
Delphi
has a unique kind of variable, declared with
threadvar
instead of var
. The
difference is that a threadvar
variable has a
separate value in each thread of a multithreaded application. An
ordinary variable has a single value that is shared among all
threads. A threadvar
variable must be declared at
the unit level.
Delphi implements threadvar
variables using thread
local storage (TLS) in the Windows API. The advantage of using
threadvar
instead of directly using TLS is that
Windows has a small number of TLS slots available, but you can
declare any number and size of threadvar
variables. More important, you can use threadvar
variables the way you would any other variable, which is much easier
than messing around with TLS. You can read more about
threadvar
and its uses in Chapter 4.
Exception Handling
Exceptions let you interrupt a program’s normal flow of
control. You can raise an exception in any function, procedure, or
method. The exception causes control to jump to an earlier point in
the same routine or in a routine farther back in the call stack.
Somewhere in the stack must be a routine that uses a
try
-except
-end
statement to catch the exception, or else Delphi calls
ExceptProc
to handle the
exception.
Delphi has two related statements for
dealing with exceptions. The
try
-except
statement sets up an
exception handler that gets control when something goes wrong. The
try
-finally
statement does not
handle exceptions explicitly, but guarantees that the code in the
finally
part of the statement always runs, even if
an exception is raised. Use
try
-except
to deal with errors.
Use try
-finally
when you have a
resource (such as allocated memory) that must be cleaned up properly,
no matter what happens. The
try
-except
statement is similar
to try-catch in C++ or Java. Standard C++ does not have
finally
, but Java does. Some C++ compilers,
including Borland’s, extend the C++ standard to add the same
functionality, e.g., with the __finally
keyword.
Like C++ and Java, Delphi’s
try
-except
statement can handle
all exceptions or only exceptions of a certain kind. Each
try
-except
statement can
declare many on
sections, where each section
declares an exception class. Delphi searches the
on
sections in order, trying to find an exception
class that matches, or is a superclass of, the exception
object’s class. Example 1-10 shows an example
of how to use try
-except
.
function ComputeSomething: begin try PerformSomeDifficultComputation; except on Ex: EDivideByZero do WriteLn('Divide by zero error'); on Ex: EOverflow do WriteLn('Overflow error'); else raise; // reraise the same exception, to be handled elsewhere end; end;
In a multithreaded application, each thread can maintain its own exception information and can raise exceptions independently from the other threads. See Chapter 4 for details.
When
your code raises an exception, it must pass an object to the
raise
statement. Usually, a program creates a new
exception object as part of the raise
statement,
but in rare circumstances, you might want to raise an object that
already exists. Delphi searches the call stack to find
try
statements. When it finds a
try
-finally
, it executes the
code in the finally
part of the statement, then
continues to search the stack for an exception handler. When the
stack unwinds to a try
-except
block, Delphi searches the on
sections to find one
that matches the exception object. If there are no
on
sections, Delphi runs the code in the
except
part of the statement. If there are
on
sections, Delphi tries to find a match, or it
runs the code in the else
part of the
except
block.
The variable that
is declared in the on
statement contains a
reference to the exception object. Delphi automatically frees the
object after the exception handler finishes. (See Chapter 2 for more information on objects.)
If
Delphi reaches the end of the call stack without finding a matching
exception handler, it calls ExceptProc
.
ExceptProc
is actually a pointer variable,
pointing to a procedure of two arguments: the exception object and
the address where the exception occurred. For example, you might want
to record unhandled exceptions in a special log file, as shown in
Example 1-11.
var LogFileName: string = 'C:\log.txt'; procedure LogExceptProc(ExceptObject: TObject; ErrorAddr: Pointer); const Size = 1024; resourcestring Title = 'Internal error: Please report to technical support'; var Buffer: PChar[0..Size-1]; F: TextFile; begin ExceptionErrorMessage(ExceptObject, ExceptAddr, Buffer, Size); AssignFile(F, LogFileName); if FileExists(LogFileName) then AppendFile(F) else Rewrite(F); WriteLn(F, Buffer); CloseFile(F); MessageBox(0, Buffer, Title, Mb_IconStop); end; ... // Tell Delphi to use your exception procedure. ExceptProc := @LogExceptProc;
Delphi also catches runtime errors, such as stack overflow, and calls
ErrorProc
for each one. Note that
ErrorProc
is actually a pointer variable whose
value is a procedure pointer. To set up an error handler, declare a
procedure and assign its address to
ErrorProc
.
The
System
unit deals with two kinds of error codes:
internal and external. If you write an ErrorProc
procedure, it must deal with internal error codes. These are small
numbers, where each number indicates a kind of error. Chapter 6 lists all the internal error codes.
Delphi’s default ErrorProc
maps internal
error codes to external error codes. External error codes are
documented in Delphi’s help files and are visible to the user.
Chapter 6 also lists the external error codes.
When Delphi calls ErrorProc
, it passes two
arguments: the error code and the instruction address where the error
occurred. Your error handler might look like the following, for
example:
procedure DumbErrorProc(ErrorCode: Integer; ErrorAddr: Pointer); begin ShowMessage(Format('Runtime error %d at %p', [ErrorCode, ErrorAddr])); end; ... ErrorProc := @DumbErrorProc;
Tip
The SysUtils
unit provides extra help for working
with exceptions and runtime errors. In particular, it defines
ErrorProc
and ExceptProc
procedures. ErrorProc
turns a runtime error into
an exception, such as EStackOverflow
for a stack
overflow error. The ExceptProc
routine displays
the exception message, then halts the program. In a console
application, the exception message is written to the standard output,
and in GUI applications, it is displayed in a dialog
box.
The SysUtils
unit
sets up the ErrorProc
and
ExceptProc
routines in its initialization section.
If your application raises an exception or runtime error before the
SysUtils
unit is initialized, you won’t get
the benefit of its routines and exception handlers. Therefore, when
your application reports a raw runtime error, not wrapped as an
exception, your problem probably lies in an initialization or
finalization section.
To raise an exception, use the raise
statement,
followed by an object reference. Usually, the
raise
statement creates a brand-new object. You
can create an object of any class to use as the exception object,
although most programs use SysUtils.Exception
or
one of its derived
classes.
Delphi keeps track of information about an exception, where it was
raised, the program’s context when it was raised, and so on.
You can access this information from various variables in the
System
unit. The full details are explained in
Chapter 5, but Table 1-3
presents an overview of the relevant variables.
When
an exception unwinds the call stack, Delphi calls the code in the
finally
part of each enclosing
try
-finally
block. Delphi also
cleans up the memory for dynamic arrays, long strings, wide strings,
interfaces, and Variant
s that have gone out of
scope. (Strictly speaking, it decreases the reference counts, so the
actual memory is freed only if there are no other references to the
string or array.)
If a finally
block raises an exception, the old
exception object is freed, and Delphi handles the new exception.
The most common use for a
try
-finally
statement is to
free objects and release other resources. If a routine has multiple
objects to free, it’s usually simplest to initialize all
variables to nil
, and use a single
try
-finally
block to free all
the objects at once. If an object’s destructor is likely to
raise an exception, though, you should use nested
try
-finally
statements, but in
most cases the technique shown in Example 1-12 works
well.
// Copy a file. If the source file cannot be opened, or the // destination file cannot be created, raise EFileCopyError, // and include the original error message in the new exception // message. The new message gives a little more information // than the original message. type EFileCopyError = class(EStreamError); procedure CopyFile(const ToFile, FromFile: string); var FromStream, ToStream: TFileStream; resourcestring sCannotRead = 'Cannot read file: %s'; sCannotCreate = 'Cannot create file: %s'; begin ToStream := nil; FromStream := nil; try try FromStream := TFileStream.Create(FromFile, fmOpenRead); except // Handle EFopenError exceptions, but no other kind of exception. on Ex: EFOpenError do // Raise a new exception. raise EFileCopyError.CreateFmt(sCannotRead, [Ex.Message]); end; try ToStream := TFileStream.Create(ToFile, fmCreate); except on Ex: EFCreateError do raise EFileCopyError.CreateFmt(sCannotCreate, [Ex.Message]); end; // Now copy the file. ToStream.CopyFrom(FromStream, 0); finally // All done. Close the files, even if an exception was raised. ToStream.Free; FromStream.Free; end; end;
File I/O
Traditional Pascal file I/O works in
Delphi, but you cannot use the standard Input
and
Output
files in a GUI application. To assign a
filename to a File
or TextFile
variable, use AssignFile
. Reset
and Rewrite
work as they do in standard Pascal, or
you can use Append
to open a file to append to its
end. The file must already exist. To close the file, use
CloseFile
. Table 1-4 lists the
I/O procedures Delphi provides.
Routine |
Description |
Write data to a file. | |
Read a line of data from a text file. | |
Open a file for writing, erasing the previous contents. | |
Change the file position. | |
Write formatted data. | |
Write a line of text. |
When you open a file with Reset
, the
FileMode
variable dictates the mode for opening
the file. By default, FileMode
is 2, which allows
read and write access. If you just want to read a file, you should
set FileMode
to
before calling Reset
. (Set
FileMode
to 1 for write-only
access.)
Delphi’s runtime library has a better way to do file I/O using
streams. Streams are object oriented and offer much more flexibility
and power than traditional Pascal I/O. The only time not to use
streams is when you cannot use the library and must stick to the
Delphi Pascal language only. Chapter 5 presents
all the file I/O procedures. Read about TStream
and related stream classes in Delphi’s online help
files.
Functions and Procedures
Delphi supports several extensions to standard Pascal functions and procedures. You can overload routines by declaring multiple routines with the same name, but different numbers or types of parameters. You can declare default values for parameters, thereby making the parameters optional. Almost everything in this section applies equally to functions and procedures, so the term routine is used for both.
Overloading
You can
overload a routine name by declaring multiple routines with the same
name, but with different arguments. To declare overloaded routines,
use the overload
directive, for example:
function AsString(Int: Integer): string; overload; function AsString(Float: Extended): string; overload; function AsString(Float: Extended; MinWidth: Integer):string; overload; function AsString(Bool: Boolean): string; overload;
When you call an overloaded routine, the compiler must be able to tell which routine you want to call. Therefore, the overloaded routines must take different numbers or types of arguments. For example, using the declarations above, you can tell which function to call just by comparing argument types:
Str := AsString(42); // call AsString(Integer) Str := AsString(42.0); // call AsString(Extended) Str := AsString(42.0, 8); // call AsString(Extended, Integer)
Sometimes, unit A will declare a routine, and unit B uses unit A, but
also declares a routine with the same name. The declaration in unit B
does not need the overload
directive, but you
might need to use unit A’s name to qualify calls to A’s
version of the routine from unit B. A derived class that overloads a
method from an ancestor class should use the
overload
directive.
Default Parameters
Sometimes, you can use default parameters instead of overloaded routines. For example, consider the following overloaded routines:
function AsString(Float: Extended): string; overload; function AsString(Float: Extended; MinWidth: Integer):string; overload;
Most likely, the first overloaded routine converts its floating-point
argument to a string using a predefined minimum width, say, 1. In
fact, you might even write the first AsString
function so it calls the second one, for example:
function AsString(Float: Extended): string; begin Result := AsString(Float, 1) end;
You can save yourself some headaches and extra code by writing a single routine that takes an optional parameter. If the caller does not provide an actual argument, Delphi substitutes a default value:
function AsString(Float: Extended; MinWidth: Integer = 1): string;
Judicious use of default parameters can save you from writing extra
overloaded routines. Be careful when using string-type parameters,
though. Delphi must compile the string everywhere the routine is
called with the default parameter. This isn’t a problem if the
string is empty (because Delphi represents an empty string with a
nil
pointer), but if the string is not empty, you
should use an initialized variable (or typed constant). That way,
Delphi can store a reference to the variable when it needs to use the
default parameter. The alternative is to let Delphi waste space
storing extra copies of the string and waste time creating a new
instance of the string for each function call.
Result Variable
Delphi
borrows a feature from the Eiffel language, namely the
Result
variable. Every function implicitly
declares a variable, named Result
, whose type is
the function’s return type. You can use this variable as an
ordinary variable, and when the function returns, it returns the
value of the Result
variable. Using
Result
is more convenient than assigning a value
to the function name, which is the standard Pascal way to return a
function result. Because Result
is a variable, you
can get and use its value repeatedly. In standard Pascal, you can do
the same by declaring a result variable explicitly, provided you
remember to assign the result to the function name. It doesn’t
make a big difference, but the little niceties can add up in a large
project. Delphi supports the old way of returning a function result,
so you have a choice. Whichever approach you choose, be consistent.
Example 1-13 shows two different ways to compute a
factorial: the Delphi way and the old-fashioned way.
// Computing a factorial in Delphi. function Factorial(Number: Cardinal): Int64; var N: Cardinal; begin Result := 1; for N := 2 to Number do Result := Result * N; end; // Computing a factorial in standard Pascal. function Factorial(Number: Integer): Integer; var N, Result: Integer; begin Result := 1; for N := 2 to Number do Result := Result * N; Factorial := Result; end;
Warning
Delphi usually initializes string and dynamic array variables, but
Result
is special. It’s not really a local
variable, but is more like a hidden var
parameter.
In other words, the caller must initialize it. The problem is that
Delphi does not always initialize
Result
. To be safe, if your function returns a
string, interface, dynamic array, or Variant
type,
initialize the Result
variable to an empty string,
array, or Unassigned
.
Get Delphi in a Nutshell now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.