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.

Table 1-1. Delphi Files

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 resourcestring declarations

.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.

Example 1-1. Showing the Order of Unit Initialization
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.

Example 1-2. The Output from Running Example 1-1
W:\nutshell>example1_1
unitB initialization
unitA initialization
Example 1-1 main program
unitA finalization
unitB finalization

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.

Example 1-3. A Typical Program 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.

Example 1-4. DLL Attach and Detach Viewer
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.

Example 1-5. Sample Package Source File
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:

  • Instead of bit masks, sets are usually easier to read.

  • You can use pointers instead of arrays, but arrays are easier and offer bounds-checking.

  • Records are the equivalent of structures, and variant records are like unions.

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.

Table 1-2. Standard Integer Types

Type

Size

Range in Delphi 5

Integer

natural

-2,147,483,648 .. 2,147,483,647

Cardinal

natural

0 .. 4,294,967,295

ShortInt

8 bits

-128 .. 127

Byte

8 bits

0 .. 255

SmallInt

16 bits

-32,768 .. 32,767

Word

16 bits

0 .. 65,535

LongInt

32 bits

-2,147,483,648 .. 2,147,483,647

LongWord

32 bits

0 .. 4,294,967,295

Int64

64 bits

-9,223,372,036,854,775,808 .. 9,223,372,036,854,775,807

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.

Example 1-6. Using Int64 to Store Currency Values
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.

Example 1-7. Using 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.

Example 1-8. Converting Type Variant Data 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 Variants 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 Variants 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 Variants 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.

Example 1-9. Using a Pointer to Create a Palette
// 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.

Example 1-10. Using try-except to Handle an Exception
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.

Example 1-11. Logging Unhandled Exceptions to a File
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.

Table 1-3. Exception and Error-Related Variables

Declaration

Description

AbstractErrorProc

Abstract method error handler.

AssertErrorProc

Assertion error handler.

ErrorAddr

Address of runtime error.

ErrorProc

Error handler procedure.

ExceptClsProc

Map a Windows exception to a Delphi class.

ExceptionClass

Exception base class.

ExceptObjProc

Map a Windows exception to a Delphi object.

ExceptProc

Unhandled exception handler.

SafeCallErrorProc

Safecall error handler.

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 Variants 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.

Example 1-12. Using try-finally to Free Multiple Objects
// 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.

Table 1-4. File I/O Procedures and Functions

Routine

Description

Append

Open an existing file for appending.

AssignFile or Assign

Assign a filename to a File or TextFile variable.

BlockRead

Read data from a file.

BlockWrite

Write data to a file.

CloseFile or Close

Close an open file.

Eof

Returns True for end of file.

Erase

Delete a file.

FilePos

Return the current file position.

FileSize

Return the size of a file, in records.

Read

Read formatted data from a file or text file.

ReadLn

Read a line of data from a text file.

Rename

Rename a file.

Reset

Open a file for reading.

Rewrite

Open a file for writing, erasing the previous contents.

Seek

Change the file position.

Write

Write formatted data.

WriteLn

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.

Warning

Delphi does not support the standard Pascal procedures Get and Put.

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.

Example 1-13. Using the Result Variable
// 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.