Search the Catalog
Delphi in a Nutshell

Delphi in a Nutshell

By Ray Lischner
1st Edition March 2000
1-56592-659-5, Order Number: 6595
600 pages, $29.95

Chapter 2
The Delphi Object Model

Delphi's support for object-oriented programming is rich and powerful. In addition to traditional classes and objects, Delphi also has interfaces (similar to those found in COM and Java), exception handling, and multithreaded programming. This chapter covers Delphi's object model in depth. You should already be familiar with standard Pascal and general principles of object-oriented programming.

Classes and Objects

Think of a class as a record on steroids. Like a record, a class describes a type that comprises any number of parts, called fields. Unlike a record, a class can also contain functions and procedures (called methods), and properties. A class can inherit from another class, in which case it inherits all the fields, methods, and properties of the ancestor class.

An object is a dynamic instance of a class. An object is always allocated dynamically, on the heap, so an object reference is like a pointer (but without the usual Pascal caret operator). When you assign an object reference to a variable, Delphi copies only the pointer, not the entire object. When your program finishes using an object, it must explicitly free the object. Delphi does not have any automatic garbage collection (but see the section "Interfaces," later in this chapter).

For the sake of brevity, the term object reference is often shortened to object, but in precise terms, the object is the chunk of memory where Delphi stores the values for all the object's fields. An object reference is a pointer to the object. The only way to use an object in Delphi is through an object reference. An object reference usually comes in the form of a variable, but it might also be a function or property that returns an object reference.

A class, too, is a distinct entity (as in Java, but unlike C++). Delphi's representation of a class is a read-only table of pointers to virtual methods and lots of information about the class. A class reference is a pointer to the table. (Chapter 3, Runtime Type Information, describes in depth the layout of the class tables.) The most common use for a class reference is to create objects or to test the type of an object reference, but you can use class references in many other situations, including passing class references as routine parameters or returning a class reference from a function. The type of a class reference is called a metaclass.

Example 2-1 shows several class declarations. A class declaration is a type declaration that starts with the keyword class. The class declaration contains field, method, and property declarations, ending with the end keyword. Each method declaration is like a forward declaration: you must implement the method in the same unit (except for abstract methods, which are discussed later in this chapter).

Example 2-1: Examples of Classes and Objects
type
  TAccount = class
  private
    fCustomer: string;  // name of customer
    fNumber: Cardinal;  // account number
    fBalance: Currency; // current account balance
  end;
  TSavingsAccount = class(TAccount)
  private
    fInterestRate: Integer; // annual percentage rate, scaled by 1000
  end;
  TCheckingAccount = class(TAccount)
  private
    fReturnChecks: Boolean;
  end;
  TCertificateOfDeposit = class(TSavingsAccount)
  private
    fTerm: Cardinal;  // CD maturation term, in days
  end;
 
var
  CD1, CD2: TAccount;
begin
  CD1 := TCertificateOfDeposit.Create;
  CD2 := TCertificateOfDeposit.Create;
  ...

Figure 2-1 depicts the memory layout of the objects and classes from Example 2-1. The variables and their associated objects reside in read-write memory. Classes reside in read-only memory, along with the program code.

Figure 2-1. The memory layout of objects and classes

 

Delphi's object model is similar to those in other object-oriented languages, such as C++ and Java. Table 2-1 shows a quick comparison between Delphi and several other popular programming languages.

Table 2-1: Delphi Versus the World

Language Feature

Delphi

Java

C++

Visual Basic

Inheritance

 

Multiple inheritance

 

 

 

Interfaces

[1]

Single root class

 

 

Metaclasses

 

 

Class (static) fields

 

 

Virtual methods

 

Abstract (pure) virtual methods

 

Class (static) methods

 

Dynamic methods

 

 

 

Garbage collection

[2]

 

[2]

Variant types

 

 

OLE automation

 

 

Static type-checking

 

Exception handling

Function overloading

 

Operator overloading

 

 

 

Non-class functions

 

Non-object variables

 

Properties

 

 

Runtime type information

[3]

 

Generic types (templates)

 

 

 

Built-in support for threads

 

 

Message passing

 

 

 

Built-in assembler

 

[4]

 

Inline functions

 

 

 

The following sections explain each of these language features in more detail.

Classes

A class declaration is a kind of type declaration. A class declaration describes the fields, methods, and properties of the class. You can declare a class in an interface or implementation section of a unit, but the methods--like any other function or procedure--are defined in the implementation section. You must implement a class's methods in the same unit as the class declaration.

A class declaration has one or more sections for different access levels (private, protected, public, published, or automated). Access levels are discussed later in this chapter. You can mix sections in any order and repeat sections with the same access level.

Within each section, you can have any number of fields, followed by method and property declarations. Method and property declarations can be mixed together, but all fields must precede all methods and properties within each section. Unlike Java and C++, you cannot declare any types nested inside a class declaration.

A class has a single base class, from which it inherits all the fields, properties, and methods. If you do not list an explicit base class, Delphi uses TObject. A class can also implement any number of interfaces. Thus, Delphi's object model most closely resembles that of Java, where a class can extend a single class and implement many interfaces.

TIP:  

The convention in Delphi is that type names begin with the letter T, as in TObject. It's just a convention, not a language rule. The IDE, on the other hand, always names form classes with an initial T.

A class reference is an expression that refers to a specific class. A class reference is not quite a first class object, as it is in Java or Smalltalk, but is used to create new objects, call class methods, and test or cast an object's type. A class reference is implemented as a pointer to a table of information about the class, especially the class's virtual method table (VMT). (See Chapter 3 for the complete details of what's inside a VMT.)

The most common use for a class reference is to create instances of that class by calling a constructor. You can also use a class reference to test the type of an object (with the is operator) or to cast an object to a particular type (with the as operator). Usually, the class reference is a class name, but it can also be a variable whose type is a metaclass, or a function or property that returns a class reference. Example 2-2 shows an example of a class declaration.

Example 2-2: Declaring a Class and Metaclass
type
  TComplexClass = class of TComplex; // metaclass type
  TComplex = class(TPersistent)
  private
    fReal, fImaginary: Double;
  public
    constructor Create(Re: Double = 0.0); overload;
    constructor Create(Re, Im: Double); overload;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    function AsString: string;
  published
    property Real: Double read fReal write fReal;
    property Imaginary: Double read fImaginary write fImaginary;
  end;

Objects

An object is a dynamic instance of a class. The dynamic instance contains values for all the fields declared in the class and all of its ancestor classes. An object also contains a hidden field that stores a reference to the object's class.

Objects are always allocated dynamically, on the heap, so an object reference is really a pointer to the object. The programmer is responsible for creating objects and for freeing them at the appropriate time. To create an object, use a class reference to call a constructor, for example:

Obj := TSomeClass.Create;

Most constructors are named Create, but that is a convention, not a requirement of Delphi. You will sometimes find constructors with other names, especially older classes that were written before Delphi had method overloading. For maximum compatibility with C++ Builder, which does not let you name constructors, you should stick with Create for all your overloaded constructors.

To get rid of the object when your program no longer needs it, call the Free method. To ensure that the object is properly freed, even if an exception is raised, use a try-finally exception handler. (See Chapter 1, Delphi Pascal, for more information about try-finally.) For example:

Obj := TSomeOtherClass.Create;
try
  Obj.DoSomethingThatMightRaiseAnException;
  Obj.DoSomethingElse;
finally
  Obj.Free;
end;

When freeing a global variable or field, always set the variable to nil when freeing the object so you are not left with a variable that contains an invalid pointer. You should take care to set the variable to nil before freeing the object. If the destructor, or a method called from the destructor, refers to that variable, you usually want the variable to be nil to avoid any potential problems. An easy way to do this is to call the FreeAndNil procedure (from the SysUtils unit):

GlobalVar := TFruitWigglies.Create;
try
  GlobalVar.EatEmUp;
finally
  FreeAndNil(GlobalVar);
end;

Each object has a separate copy of all of its fields. A field cannot be shared among multiple objects. If you need to share a variable, declare the variable at the unit level or use indirection: many objects can hold separate pointers or object references that refer to common data.

Inheritance

A class can inherit from another class. The derived class inherits all the fields, methods, and properties of the base class. Delphi supports only single inheritance, so a class has one base class. That base class can have its own base class, and so on, so a class inherits the fields, properties, and methods of every ancestor class. A class can also implement any number of interfaces (which are covered later in this chapter). As in Java, but not C++, every class inherits from a single root class, TObject. If you do not specify an explicit base class, Delphi automatically uses TObject as the base class.

TIP:  

A base class is a class's immediate parent class, which you can see in the class declaration. An ancestor class is the base class or any other class in the inheritance chain up to TObject. Thus, in Example 2-1, TCertificateOfDeposit has a base class of TSavingsAccount; its ancestor classes are TObject, TAccount, and TSavingsAccount.

The TObject class declares several methods and one special, hidden field to store a reference to the object's class. This hidden field points to the class's virtual method table (VMT). Every class has a unique VMT and all objects of that class share the class's VMT. Chapter 5, Language Reference, covers the other details of the TObject class and its methods.

You can assign an object reference to a variable whose type is the object's class or any of its ancestor classes. In other words, the declared type of an object reference is not necessarily the same as the actual type of the object. Assignments that go the other way--assigning a base-class object reference to a derived-class variable--are not allowed because the object might not be of the correct type.

Delphi retains the strong type-checking of Pascal, so the compiler performs compile-time checks based on the declared type of an object reference. Thus, all methods must be part of the declared class, and the compiler performs the usual checking of function and procedure arguments. The compiler does not necessarily bind the method call to a specific method implementation. If the method is virtual, Delphi waits until runtime and uses the object's true type to determine which method implementation to call. See the section "Methods," later in this chapter for details.

Use the is operator to test the object's true class. It returns True if the class reference is the object's class or any of its ancestor classes. It returns False if the object reference is nil or of the wrong type. For example:

if Account is TCheckingAccount then ... // tests the class of Account
if Account is TObject then ...          // True when Account is not nil

You can also use a type cast to obtain an object reference with a different type. A type cast does not change an object; it just gives you a new object reference. Usually, you should use the as operator for type casts. The as operator automatically checks the object's type and raises a runtime error if the object's class is not a descendant of the target class. (The SysUtils unit maps the runtime error to an EInvalidCast exception.)

Another way to cast an object reference is to use the name of the target class in a conventional type cast, similar to a function call. This style of type cast does not check that the cast is valid, so use it only if you know it is safe, as shown in Example 2-3.

Example 2-3: Using Static Type Casts
var
  Account: TAccount;
  Checking: TCheckingAccount;
begin
  Account  := Checking;                    // Allowed
  Checking := Account;                     // Compile-time error
  Checking := Account as TCheckingAccount; // Okay
  Account as TForm;                        // Raises a runtime error
  Checking := TCheckingAccount(Account);   // Okay, but not recommended
  if Account is TCheckingAccount then      // Better
    Checking := TCheckingAccount(Account)
  else
    Checking := nil;

Fields

A field is a variable that is part of an object. A class can declare any number of fields, and each object has its own copy of every field declared in its class and in every ancestor class. In other languages, a field might be called a data member, an instance variable, or an attribute. Delphi does not have class variables, class instance variables, static data members, or the equivalent (that is, variables that are shared among all objects of the same class). Instead, you can usually use unit-level variables for a similar effect.

A field can be of any type unless the field is published. In a published section, a field must have a class type, and the class must have runtime type information (that is, the class or an ancestor class must use the $M+ directive). See Chapter 3 for more information.

When Delphi first creates an object, all of the fields start out empty, that is, pointers are initialized to nil, strings and dynamic arrays are empty, numbers have the value zero, Boolean fields are False, and Variants are set to Unassigned. (See NewInstance and InitInstance in Chapter 5 for details.)

A derived class can declare a field with the same name as a field in an ancestor class. The derived class's field hides the field of the same name in the ancestor class. Methods in the derived class refer to the derived class's field, and methods in the ancestor class refer to the ancestor's field.

Methods

Methods are functions and procedures that apply only to objects of a particular class and its descendants. In C++, methods are called "member functions." Methods differ from ordinary procedures and functions in that every method has an implicit parameter called Self, which refers to the object that is the subject of the method call. Self is similar to this in C++ and Java. Call a method the same way you would call a function or procedure, but preface the method name with an object reference, for example:

Object.Method(Argument);

A class method applies to a class and its descendants. In a class method, Self refers not to an object but to the class. The C++ term for a class method is "static member function."

You can call a method that is declared in an object's class or in any of its ancestor classes. If the same method is declared in an ancestor class and in a derived class, Delphi calls the most-derived method, as shown in Example 2-4.

Example 2-4: Binding Static Methods
type
  TAccount = class
  public
    procedure Withdraw(Amount: Currency);
  end;
  TSavingsAccount = class(TAccount)
  public
    procedure Withdraw(Amount: Currency);
  end;
var
  Savings: TSavingsAccount;
  Account: TAccount;
begin
  ...
  Savings.Withdraw(1000.00);     // Calls TSavingsAccount.Withdraw
  Account.Withdraw(1000.00);     // Calls TAccount.Withdraw

An ordinary method is called a static method because the compiler binds the method call directly to a method implementation. In other words, the binding is static. In C++ this is an ordinary member function, and in Java it's called a "final method." Most Delphi programmers refrain from using the term static method, preferring the simple term, method or even non-virtual method.

A virtual method is a method that is bound at runtime instead of at compile time. At compile time, Delphi uses the declared type of an object reference to determine which methods you are allowed to call. Instead of compiling a direct reference to any specific method, the compiler stores an indirect method reference that depends on the object's actual class. At runtime, Delphi looks up the method in the class's runtime tables (specifically, the VMT), and calls the method for the actual class. The object's true class might be the compile-time declared class, or it might be a derived class--it doesn't matter because the VMT provides the pointer to the correct method.

To declare a virtual method, use the virtual directive in the base class, and use the override directive to provide a new definition of the method in a derived class. Unlike in Java, methods are static by default, and you must use the virtual directive to declare a virtual method. Unlike in C++, you must use the override directive to override a virtual method in a derived class.

Example 2-5 uses virtual methods.

Example 2-5: Binding Virtual Methods
type
  TAccount = class
  public
    procedure Withdraw(Amount: Currency); virtual;
  end;
  TSavingsAccount = class(TAccount)
  public
    procedure Withdraw(Amount: Currency); override;
  end;
var
  Savings: TSavingsAccount;
  Account: TAccount;
begin
  ...
  Savings.Withdraw(1000.00);     // Calls TSavingsAccount.Withdraw
  Account := Savings;
  Account.Withdraw(1000.00);     // Calls TSavingsAccount.Withdraw

Instead of using the virtual directive, you can also use the dynamic directive. The semantics are identical, but the implementation is different. Looking up a virtual method in a VMT is fast because the compiler generates an index directly into a VMT. Looking up a dynamic method is slower. Calling a dynamic method requires a linear search of a class's dynamic method table (DMT). If the class does not override that method, the search continues with the DMT of the base class. The search continues with ancestor classes until TObject is reached or the method is found. The tradeoff is that in a few circumstances, dynamic methods take up less memory than virtual methods. Unless you are writing a replacement for the VCL, you should use virtual methods, not dynamic methods. See Chapter 3 for a complete explanation of how dynamic and virtual methods are implemented.

A virtual or dynamic method can be declared with the abstract directive, in which case the class does not define the method. Instead, derived classes must override that method. The C++ term for an abstract method is a "pure virtual method." If you call a constructor for a class that has an abstract method, the compiler issues a warning, telling you that you probably made a mistake. You probably wanted to create an instance of a derived class that overrides and implements the abstract method. A class that declares one or more abstract methods is often called an abstract class, although some people reserve that term for a class that declares only abstract methods.

TIP:  

If you write an abstract class that inherits from another abstract class, you should redeclare all abstract methods with the override and abstract directives. Delphi does not require this, but common sense does. The declarations clearly inform the maintainer of the code that the methods are abstract. Otherwise, the maintainer must wonder whether the methods should have been implemented or should have remained abstract. For example:

type
  TBaseAbstract = class
    procedure Method; virtual; abstract;
  end;
  TDerivedAbstract = class(TBaseAbsract)
    procedure Method; override; abstract;
  end;
  TConcrete = class(TDerivedAbstract)
    procedure Method; override;
  end; 

A class method or constructor can also be virtual. In Delphi, class references are real entities that you can assign to variables, pass as parameters, and use as references for calling class methods. If a constructor is virtual, a class reference can have a static type of the base class, but you can assign to it a class reference for a derived class. Delphi looks up the virtual constructor in the class's VMT and calls the constructor for the derived class.

Methods (and other functions and procedures) can be overloaded, that is, multiple routines can have the same name, provided they take different arguments. Declare overloaded methods with the overload directive. A derived class can overload a method it inherits from a base class. In that case, only the derived class needs the overload directive. After all, the author of the base class cannot predict the future and know when other programmers might want to overload an inherited method. Without the overload directive in the derived class, the method in the derived class hides the method in the base class, as shown in Example 2-6.

Example 2-6: Overloading Methods
type
  TAuditKind = (auInternal, auExternal, auIRS, auNasty);
  TAccount = class
  public
    procedure Audit;
  end;
  TCheckingAccount = class(TAccount)
  public
    procedure Audit(Kind: TAuditKind); // Hides TAccount.Audit
  end;
  TSavingsAccount = class(TAccount)
  public
    // Can call TSavingsAccount.Audit and TAccount.Audit
    procedure Audit(Kind: TAuditKind); overload;
  end;
var
  Checking: TCheckingAccount;
  Savings: TSavingsAccount;
begin
  Checking := TCheckingAccount.Create;
  Savings := TSavingsAccount.Create;
  Checking.Audit;             // Error because TAccount.Audit is hidden
  Savings.Audit;              // Okay because Audit is overloaded
  Savings.Audit(auNasty);     // Okay
  Checking.Audit(auInternal); // Okay

Constructors

Every class has one or more constructors, possibly inherited from a base class. By convention, constructors are usually named Create, although you can use any name you like. Some constructor names start with Create, but convey additional information, such as CreateFromFile or CreateFromStream. Usually, though, the simple name Create is sufficient, and you can use method overloading to define multiple constructors with the same name. Another reason to overload the name Create is for compatibility with C++ Builder. C++ does not permit different constructor names, so you must use overloading to define multiple constructors.

Calling a constructor

A constructor is a hybrid of object and class methods. You can call it using an object reference or a class reference. Delphi passes an additional, hidden parameter to indicate how it was called. If you call a constructor using a class reference, Delphi calls the class's NewInstance method to allocate a new instance of the class. After calling NewInstance, the constructor continues and initializes the object. The constructor automatically sets up a try-except block, and if any exception occurs in the constructor, Delphi calls the destructor.

When you call a constructor with an object reference, Delphi does not set up the try-except block and does not call NewInstance. Instead, it calls the constructor the same way it calls any ordinary method. This lets you call an inherited constructor without unnecessary overhead.

TIP:  

A common error is to try to create an object by calling a constructor with an object reference, rather than calling it with a class reference and assigning it to the object variable:

var
  Account: TSavingsAccount;
begin
  Account.Create;                    // wrong
  Account := TSavingsAccount.Create; // right

One of Delphi's features is that you have total control over when, how, and whether to call the inherited constructor. This lets you write some powerful and interesting classes, but also introduces an area where it is easy to make mistakes.

Delphi always constructs the derived class first, and only if the derived class calls the inherited constructor does Delphi construct the base class. C++ constructs classes in the opposite direction, starting from the ancestor class and constructing the derived class last. Thus, if class C inherits from B, which inherits from A, Delphi constructs C first, then B, and A last. C++ constructs A first, then B, and finally C.

Virtual methods and constructors

Another significant difference between C++ and Delphi is that in C++, a constructor always runs with the virtual method table of the class being constructed, but in Delphi, the virtual methods are those of the derived class, even when the base class is being constructed. As a result, you must be careful when writing any virtual method that might be called from a constructor. Unless you are careful, the object might not be fully constructed when the method is called. To avoid any problems, you should override the AfterConstruction method and use that for any code that needs to wait until the object is fully constructed. If you override AfterConstruction, be sure to call the inherited method, too.

One constructor can call another constructor. Delphi can tell the call is from an object reference (namely, Self), so it calls the constructor as an ordinary method. The most common reason to call another constructor is to put all the initialization code in a single constructor. Example 2-7 shows some different ways to define and call constructors.

Example 2-7: Declaring and Calling Constructors
type
  TCustomer = class ... end;
  TAccount = class
  private
    fBalance: Currency;
    fNumber: Cardinal;
    fCustomer: TCustomer;
  public
    constructor Create(Customer: TCustomer); virtual;
    destructor Destroy; override;
  end;
  TSavingsAccount = class(TAccount)
  private
    fInterestRate: Integer; // Scaled by 1000
  public
    constructor Create(Customer: TCustomer); override; overload;
    constructor Create(Customer: TCustomer; InterestRate: Integer);
        overload;
    // Note that TSavingsAccount does not need a destructor. It simply
    // inherits the destructor from TAccount.
  end;
 
var
  AccountNumber: Cardinal = 1;
 
constructor TAccount.Create(Customer: TCustomer);
begin
  inherited Create;             // Call TObject.Create.
  fNumber := AccountNumber;     // Assign a unique account number.
  Inc(AccountNumber);
  fCustomer := Customer;        // Notify customer of new account.
  Customer.AttachAccount(Self);
end;
 
destructor TAccount.Destroy;
begin
  // If the constructor fails before setting fCustomer, the field
  // will be nil. Release the account only if Customer is not nil.
  if Customer <> nil then
    Customer.ReleaseAccount(Self);
  // Call TObject.Destroy.
  inherited Destroy;
end;
 
const
  DefaultInterestRate = 5000;  // 5%, scaled by 1000
 
constructor TSavingsAccount.Create(Customer: TCustomer);
begin
  // Call a sibling constructor.
  Create(Customer, DefaultInterestRate);
end;
 
constructor TSavingsAccount(Customer: TCustomer; InterestRate:Integer);
begin
  // Call TAccount.Create.
  inherited Create(Customer);
  fInterestRate := InterestRate;
end;

Destructors

Destructors, like constructors, take an extra hidden parameter. The first call to a destructor passes True for the extra parameter. This tells Delphi to call FreeInstance to free the object. If the destructor calls an inherited destructor, Delphi passes False as the hidden parameter to prevent the inherited destructor from trying to free the same object.

TIP:  

A class usually has one destructor, called Destroy. Delphi lets you declare additional destructors, but you shouldn't take advantage of that feature. Declaring multiple destructors is confusing and serves no useful purpose.

Before Delphi starts the body of the destructor, it calls the virtual method, BeforeDestruction. You can override BeforeDestruction to assert program state or take care of other business that must take place before any destructor starts. This lets you write a class safely without worrying about how or whether any derived classes will call the base class destructor.

TIP:  

When writing a class, you might need to override the Destroy destructor, but you must not redeclare the Free method. When freeing an object, you should call the Free method and not the destructor. The distinction is important, because Free checks whether the object reference is nil and calls Destroy only for non-nil references. In extraordinary circumstances, a class can redefine the Free method (such as TInterface in the seldom-used VirtIntf unit), which makes it that much more important to call Free, not Destroy.

If a constructor or AfterConstruction method raises an exception, Delphi automatically calls the object's destructor. When you write a destructor, you must remember that the object being destroyed might not have been completely constructed. Delphi ensures that all fields start out at zero, but if the exception occurs in the middle of your constructor, some fields might be initialized and some might still be zero. If the destructor just frees objects and pointers, you don't need to worry, because the Free method and FreeMem procedure both check for nil pointers. If the destructor calls other methods, though, always check first for a nil pointer.

Object Life Cycle

For most objects, you call a constructor to create the object, use the object, and then call Free to free the object. Delphi handles all the other details for you. Sometimes, though, you need to know a little more about the inner mechanisms of Delphi's object model. Example 2-8 shows the methods that Delphi calls or simulates when it creates and frees an object.

Example 2-8: The Life Cycle of an Object
type
  TSomething = class
    procedure DoSomething;
  end;
var
  Ref: TSomething;
begin
  Ref := TSomething.Create;
  Ref.DoSomething;
  Ref.Free;
end;
 
// The hidden code in the constructor looks something like this:
function TSomething.Create(IsClassRef: Boolean): TSomething;
begin
  if IsClassRef then
  try
    // Allocate the new object.
    Self := TSomething.NewInstance;
 
    // NewInstance initializes the object in the same way that
    // InitInstance does. If you override NewInstance, though,
    // and do not call the inherited NewInstance, you must call
    // InitInstance. The call is shown below, so you know what
    // happens, but remember that ordinarily Delphi does not
    // actually call InitInstance.
    InitInstance(Self);
 
    // Do the real work of the constructor, but without all the
    // class reference overhead. Delphi does not really call the
    // constructor recursively. 
    Self.Create(False);
 
    Self.AfterConstruction;
  except
    // If any exception occurs, Delphi automatically calls the
    // object's destructor.
    Self.Destroy;
  end
  else
    Self.Create(False);
  Result := Self;
end;
 
// The hidden code in the destructor looks something like this:
procedure TSomething.Destroy(Deallocate: Boolean);
begin
  if Deallocate then
    Self.BeforeDestruction;
 
  // Delphi doesn't really call the destructor recursively, but
  // this is where the destructor's real work takes place.
  Self.Destroy(False);
 
  if Deallocate then
  begin
    // Delphi doesn't really call CleanupInstance. Instead, the
    // FreeInstance method does the cleanup. If you override
    // FreeInstance and do not call the inherited FreeInstance,
    // you must call CleanupInstance to clean up strings,
    // dynamic arrays, and Variant-type fields.
    Self.CleanupInstance;
    // Call FreeInstance to free the object's memory.
    Self.FreeInstance;
  end;
end;

Access Levels

Like C++ and Java, Delphi has different access levels that determine which objects can access the fields, methods, and properties of another object. The access levels are as follows:

private
Declarations that are declared private can be accessed only by the class's own methods or by any method, procedure, or function defined in the same unit's implementation section. Delphi does not have C++-style friend declarations or Java-style package level access. The equivalent in Delphi is to declare package or friend classes in the same unit, which gives them access to the private and protected parts of every class defined in the same unit.

protected
A protected declaration can be accessed from any method of the class or its descendants. The descendent classes can reside in different units.

public
Public methods have unrestricted access. Any method, function, or procedure can access a public declaration. Unless you use the $M+ compiler directive (see Chapter 8, Compiler Directives, for details), the default access level is public.

published
Published declarations are similar to public declarations, except that Delphi stores runtime type information for published declarations. Some declarations cannot be published; see Chapter 3 for details. If a class or a base class uses the $M+ directive, the default access level is published.

TIP:  

Delphi's IDE declares fields and methods in the initial unnamed section of a form declaration. Because TForm inherits from TPersistent, which uses the $M+ directive, the initial section is published. In other words, the IDE declares its fields and methods as published. When Delphi loads a form description (.dfm file), it relies on the published information to build the form object. The IDE relies on the initial, unnamed section of the form class. If you modify that section, you run the risk of disabling the IDE's form editor.

automated
Automated declarations are similar to public declarations, except that Delphi stores additional runtime type information to support OLE automation servers. Automated declarations are obsolete; you should use Delphi's type library editor instead, but for now, they remain a part of the language for backward compatibility. A future release of Delphi might eliminate them entirely. Chapter 3 describes automated declarations in more depth.

A derived class can increase the access level of a property by redeclaring the property under the new access level (e.g., change protected to public). You cannot decrease a property's access level, and you cannot change the visibility of a field or method. You can override a virtual method and declare the overridden method at the same or higher access level, but you cannot decrease the access level.

Hiding a Constructor

Sometimes, a class is not for public use, but is a helper class whose use is entirely subservient to another class. In that case, you probably want to make the constructors for the helper class private or protected, but this is tricky. TObject declares a public constructor: Create. Even though the helper class's constructors are private or protected, you can call the public Create constructor inherited from TObject.

Although you cannot change the access level of the inherited Create constructor, you can hide it with another public constructor. Because the derived constructor should not be called, it can raise an exception. For example:

type
  TPublic = class;
  TPrivateHelper = class
  private
    // TPublic is the only class allowed to
    // call the real constructor:
    constructor Create(Owner: TPublic);
      overload;
  public
    // Hide TObject.Create, in case someone
    // accidentally tries to create a
    // TPrivateHelper instance.
    constructor Create;
      reintroduce; overload;
  end;
  TPublic = class
  private
    fHelper: TPrivateHelper;
  public
    constructor Create;
    destructor Destroy;
  end;
 
constructor TPrivateHelper.Create;
begin
  raise Exception.Create('Programming error')
end;
 
constructor TPublic.Create;
begin
  // This is the only place where
  // TPrivateHelper is created.
  fHelper := TPrivateHelper.Create(Self);
end;

 

Properties

A property looks like a field but can act like a method. Properties take the place of accessor and mutator methods (sometimes called getters and setters), but have much more flexibility and power. Properties are vital to Delphi's IDE, and you can also use properties in many other situations.

A property has a reader and writer to get and set the property's value. The reader can be the name of a field, a selector for an aggregate field, or a method that returns the property value. The writer can be a field name, a selector for an aggregate field, or a method that sets the property value. You can omit the writer to make a read-only property. You can also omit the reader to create a write-only property, but the uses for such a beast are limited. Omitting both the reader and the writer is pointless, so Delphi does not let you do so.

Most readers and writers are field names or method names, but you can also refer to part of an aggregate field (record or array). If a reader or writer refers to an array element, the array index must be a constant, and the field's type cannot be a dynamic array. Records and arrays can be nested, and you can even use variant records. Example 2-9 shows an extended rectangle type, similar to the Windows TRect type, but because it is a class, it has properties and methods.

Example 2-9: Properties Readers and Writers

TRectEx = class(TPersistent)
  private
    R: TRect;
    function GetHeight: Integer;
    function GetWidth: Integer;
    procedure SetHeight(const Value: Integer);
    procedure SetWidth(const Value: Integer);
  public
    constructor Create(const R: TRect); overload;
    constructor Create(Left, Top, Right, Bottom: Integer); overload;
    constructor Create(const TopLeft, BottomRight: TPoint); overload;
 
    procedure Assign(Source: TPersistent); override;
    
    procedure Inflate(X, Y: Integer);
    procedure Intersect(const R: TRectEx);
    function IsEmpty: Boolean;
    function IsEqual(const R: TRectEx): Boolean;
    procedure Offset(X, Y: Integer);
    procedure Union(const R: TRectEx);
 
    property TopLeft: TPoint read R.TopLeft write R.TopLeft;
    property BottomRight: TPoint read R.BottomRight write R.BottomRight;
    property Rect: TRect read R write R;
    property Height: Integer read GetHeight write SetHeight;
    property Width: Integer read GetWidth write SetWidth;
  published
    property Left: Integer read R.Left write R.Left default 0;
    property Right: Integer read R.Right write R.Right default 0;
    property Top: Integer read R.Top write R.Top default 0;
    property Bottom: Integer read R.Bottom write R.Bottom default 0;
  end;

Array properties

Properties come in scalar and array flavors. An array property cannot be published, but they have many other uses. The array index can be any type, and you can have multidimensional arrays, too. For array-type properties, you must use read and write methods--you cannot map an array-type property directly to an array-type field.

You can designate one array property as the default property. You can refer to the default property by using an object reference and an array subscript without mentioning the property name, as shown in Example 2-10.

Example 2-10: Using a Default Array Property

type
  TExample = class
    ...
    property Items[I: Integer]: Integer read GetItem write SetItem;
    property Chars[C: Char]: Char read GetChar write SetChar; default;
  end;
var
  Example: TExample;
  I: Integer;
  C: Char;
begin
  Example := TExample.Create;
  I := Example.Items[4];     // Must mention property name explicitly
  C := Example['X'];         // Array property is default
  C := Example.Chars['X'];   // Same as previous line

Indexed properties

You can map many properties to a single read or write method by specifying an index number for each property. The index value is passed to the read and write methods to differentiate one property from another.

You can even mix array indices and an index specifier. The reader and writer methods take the array indices as the first arguments, followed by the index specifier.

Default values

A property can also have stored and default directives. This information has no semantic meaning to the Delphi Pascal language, but Delphi's IDE uses this information when storing form descriptions. The value for the stored directive is a Boolean constant, a field of Boolean type, or a method that takes no arguments and returns a Boolean result. The value for the default directive is a constant value of the same type as the property. Only enumerated, integer, and set-type properties can have a default value. The stored and default directives have meaning only for published properties.

To distinguish a default array from a default value, the default array directive comes after the semicolon that ends the property declaration. The default value directive appears as part of the property declaration. See the default directive in Chapter 5 for details.

Using properties

A common approach to writing Delphi classes is to make all fields private, and declare public properties to access the fields. Delphi imposes no performance penalty for properties that access fields directly. By using properties you get the added benefit of being able to change the implementation at a future date, say to add validation when a field's value changes. You can also use properties to enforce restricted access, such as using a read-only property to access a field whose value should not be changed. Example 2-11 shows some of the different ways to declare and use properties.

Example 2-11: Declaring and Using Properties

type
  TCustomer = record
    Name: string;
    TaxIDNumber: string[9];
  end;
  TAccount = class
  private
    fCustomer: TCustomer;
    fBalance: Currency;
    fNumber: Cardinal;
    procedure SetBalance(NewBalance: Currency);
  published
    property Balance: Currency read fBalance write SetBalance;
    property Number: Cardinal read fNumber; // Cannot change account #
    property CustName: string read fCustomer.Name;
  end;
  TSavingsAccount = class(TAccount)
  private
    fInterestRate: Integer;
  published
    property InterestRate: Integer read fInterestRate
        write fInterestRate default DefaultInterestRate;
  end;
  TLinkedAccount = class(TObject)
  private
    fAccounts: array[0..1] of TAccount;
    function GetAccount(Index: Integer): TAccount;
  public
    // Two ways for properties to access an array: using an index
    // or referring to an array element.
    property Checking: TAccount index 0 read GetAccount;
    property Savings:  TAccount read fAccounts[1];
  end;
  TAccountList = class
  private
    fList: TList;
    function GetAccount(Index: Integer): TAccount;
    procedure SetAccount(Index: Integer; Account: TAccount);
    function GetCount: Integer;
  protected
    property List: TList read fList;
  public
    property Count: Integer read GetCount;
    property Accounts[Index: Integer]: TAccount read GetAccount
        write SetAccount; default;
  end;
 
procedure TAccount.SetBalance(NewBalance: Currency);
begin
  if NewBalance < 0 then
    raise EOverdrawnException.Create;
  fBalance := NewBalance;
end;
 
function TLinkedAccount.GetAccount(Index: Integer): TAccount;
begin
  Result := fAccounts[Index]
end;
 
function TAccountList.GetCount: Integer;
begin
  Result := List.Count
end;
 
function TAccountList.GetAccount(Index: Integer): TAccount;
begin
  Result := List[Index]
end;
 
procedure TAccountList.SetAccount(Index: Integer; Account: TAccount);
begin
  fList[Index] := Account
end;

Class-type properties

Properties of class type need a little extra attention. The best way to work with class-type properties is to make sure the owner object manages the property object. In other words, don't save a reference to other objects, but keep a private copy of the property object. Use a write method to store an object by copying it. Delphi's IDE requires this behavior of published properties, and it makes sense for unpublished properties, too.

The only exception to the rule for class-type properties is when a property stores a reference to a component on a form. In that case, the property must store an object reference and not a copy of the component.

Delphi's IDE stores component references in a .dfm file by storing only the component name. When the .dfm is loaded, Delphi looks up the component name to restore the object reference. If you must store an entire component within another component, you must delegate all properties of the inner component.

Make sure the property's class inherits from TPersistent and that the class overrides the Assign method. Implement your property's write method to call Assign. (TPersistent--in the Classes unit--is not required, but it's the easiest way to copy an object. Otherwise, you need to duplicate the Assign method in whatever class you use.) The read method can provide direct access to the field. If the property object has an OnChange event, you might need to set that so your object is notified of any changes. Example 2-12 shows a typical pattern for using a class-type property. The example defines a graphical control that repeatedly displays a bitmap throughout its extent, tiling the bitmap as necessary. The Bitmap property stores a TBitmap object.

Example 2-12: Declaring and Using a Class-type Property
unit Tile;
 
interface
 
uses SysUtils, Classes, Controls, Graphics;
 
type
  // Tile a bitmap
  TTile = class(TGraphicControl)
  private
    fBitmap: TBitmap;
    procedure SetBitmap(NewBitmap: TBitmap);
    procedure BitmapChanged(Sender: TObject);
  protected
    procedure Paint; override;
  public
    constructor Create(Owner: TComponent); override;
    destructor Destroy; override;
  published
    property Align;
    property Bitmap: TBitmap read fBitmap write SetBitmap;
    property OnClick;
    property OnDblClick;
    // Many other properties are useful, but were omitted to save space.
    // See TControl for a full list.
  end;
 
implementation
 
{ TTile }
 
// Create the bitmap when creating the control.
constructor TTile.Create(Owner: TComponent);
begin
  inherited;
  fBitmap := TBitmap.Create;
  fBitmap.OnChange := BitmapChanged;
end;
 
// Free the bitmap when destroying the control.
destructor TTile.Destroy;
begin
  FreeAndNil(fBitmap);
  inherited;
end;
 
// When the bitmap changes, redraw the control.
procedure TTile.BitmapChanged(Sender: TObject);
begin
  Invalidate;
end;
 
// Paint the control by tiling the bitmap. If there is no
// bitmap, don't paint anything.
procedure TTile.Paint;
var
  X, Y: Integer;
begin
  if (Bitmap.Width = 0) or (Bitmap.Height = 0) then
    Exit;
 
  Y := 0;
  while Y < ClientHeight do
  begin
    X := 0;
    while X < ClientWidth do
    begin
      Canvas.Draw(X, Y, Bitmap);
      Inc(X, Bitmap.Width);
    end;
    Inc(Y, Bitmap.Height);
  end;
end;
 
// Set a new bitmap by copying the TBitmap object.
procedure TTile.SetBitmap(NewBitmap: TBitmap);
begin
  fBitmap.Assign(NewBitmap);
end;
 
end.

Interfaces

An interface defines a type that comprises abstract virtual methods. Although a class inherits from a single base class, it can implement any number of interfaces. An interface is similar to an abstract class (that is, a class that has no fields and all of whose methods are abstract), but Delphi has extra magic to help you work with interfaces. Delphi's interfaces sometimes look like COM (Component Object Model) interfaces, but you don't need to know COM to use Delphi interfaces, and you can use interfaces for many other purposes.

You can declare a new interface by inheriting from an existing interface. An interface declaration contains method and property declarations, but no fields. Just as all classes inherit from TObject, all interfaces inherit from IUnknown. The IUnknown interface declares three methods: _AddRef, _Release, and QueryInterface. If you are familiar with COM, you will recognize these methods. The first two methods manage reference counting for the lifetime of the object that implements the interface. The third method accesses other interfaces an object might implement.

When you declare a class that implements one or more interfaces, you must provide an implementation of all the methods declared in all the interfaces. The class can implement an interface's methods, or it can delegate the implementation to a property, whose value is an interface. The simplest way to implement the _AddRef, _Release, and QueryInterface methods is to inherit them from TInterfacedObject or one of its derived classes, but you are free to inherit from any other class if you wish to define the methods yourself.

A class implements each of an interface's methods by declaring a method with the same name, arguments, and calling convention. Delphi automatically matches the class's methods with the interface's methods. If you want to use a different method name, you can redirect an interface method to a method with a different name. The redirected method must have the same arguments and calling convention as the interface method. This feature is especially important when a class implements multiple interfaces with identical method names. See the class keyword in Chapter 5 for more information about redirecting methods.

A class can delegate the implementation of an interface to a property that uses the implements directive. The property's value must be the interface that the class wants to implement. When the object is cast to that interface type, Delphi automatically fetches the property's value and returns that interface. See the implements directive in Chapter 5 for details.

For each non-delegated interface, the compiler creates a hidden field to store a pointer to the interface's VMT. The interface field or fields follow immediately after the object's hidden VMT field. Just as an object reference is really a pointer to the object's hidden VMT field, an interface reference is a pointer to the interface's hidden VMT field. Delphi automatically initializes the hidden fields when the object is constructed. See Chapter 3 to learn how the compiler uses RTTI to keep track of the VMT and the hidden field.

Reference counting

The compiler generates calls to _AddRef and _Release to manage the lifetime of interfaced objects. To use Delphi's automatic reference counting, declare a variable with an interface type. When you assign an interface reference to an interface variable, Delphi automatically calls _AddRef. When the variable goes out of scope, Delphi automatically calls _Release.

The behavior of _AddRef and _Release is entirely up to you. If you inherit from TInterfacedObject, these methods implement reference counting. The _AddRef method increments the reference count, and _Release decrements it. When the reference count goes to zero, _Release frees the object. If you inherit from a different class, you can define these methods to do anything you want. You should implement QueryInterface correctly, though, because Delphi relies on it to implement the as operator.

Typecasting

Delphi calls QueryInterface as part of its implementation of the as operator for interfaces. You can use the as operator to cast an interface to any other interface type. Delphi calls QueryInterface to obtain the new interface reference. If QueryInterface returns an error, the as operator raises a runtime error. (The SysUtils unit maps the runtime error to an EIntfCastError exception.)

You can implement QueryInterface any way you want, but you probably want to use the same approach taken by TInterfacedObject. Example 2-13 shows a class that implements QueryInterface normally, but uses stubs for _AddRef and _Release. Later in this section, you'll see how useful this class can be.

Example 2-13: Interface Base Class Without Reference Counting
type
  TNoRefCount = class(TObject, IUnknown)
  protected
    function QueryInterface(const IID:TGUID; out Obj):HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;
 
function TNoRefCount.QueryInterface(const IID:TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := Windows.E_NoInterface;
end;
 
function TNoRefCount._AddRef: Integer;
begin
  Result := -1
end;
 
function TNoRefCount._Release: Integer;
begin
  Result := -1
end;

Interfaces and object-oriented programming

The most important use of interfaces is to separate type inheritance from class inheritance. Class inheritance is an effective tool for code reuse. A derived class easily inherits the fields, methods, and properties of a base class, and thereby avoids reimplementing common methods. In a strongly typed language, such as Delphi, the compiler treats a class as a type, and therefore class inheritance becomes synonymous with type inheritance. In the best of all possible worlds, though, types and classes are entirely separate.

Textbooks on object-oriented programming often describe an inheritance relationship as an "is-a" relationship, for example, a TSavingsAccount "is-a" TAccount. You can see the same idea in Delphi's is operator, where you test whether an Account variable is TSavingsAccount.

Outside of textbook examples, though, simple is-a relationships break down. A square is a rectangle, but that doesn't mean you want to derive TSquare from TRectangle. A rectangle is a polygon, but you probably don't want to derive TRectangle from TPolygon. Class inheritance forces a derived class to store all the fields that are declared in the base class, but in this case, the derived class doesn't need that information. A TSquare object can get away with storing a single length for all of its sides. A TRectangle object, however, must store two lengths. A TPolygon object needs to store many sides and vertices.

The solution is to separate the type inheritance (a square is a rectangle is a polygon) from class inheritance (class C inherits the fields and methods of class B, which inherits the fields and methods of class A). Use interfaces for type inheritance, so you can leave class inheritance to do what it does best: inheriting fields and methods.

In other words, ISquare inherits from IRectangle, which inherits from IPolygon. The interfaces follow the "is-a" relationship. Entirely separate from the interfaces, the class TSquare implements ISquare, IRectangle, and IPolygon. TRectangle implements IRectangle and IPolygon.

TIP:  

The convention in COM programming is to name interfaces with an initial I. Delphi follows this convention for all interfaces. Note that it is a useful convention, but not a language requirement.

On the implementation side, you can declare additional classes to implement code reuse. For example, TBaseShape implements the common methods and fields for all shapes. TRectangle inherits from TBaseShape and implements the methods in a way that make sense for rectangles. TPolygon also inherits from TBaseShape and implements the methods in a way that make sense for other kinds of polygons.

A drawing program can use the shapes by manipulating IPolygon interfaces. Example 2-14 shows simplified classes and interfaces for this scheme. Notice how each interface has a GUID (Globally Unique Identifier) in its declaration. The GUID is necessary for using QueryInterface. If you need the GUID of an interface (in an explicit call to QueryInterface, for example), you can use the interface name. Delphi automatically converts an interface name to its GUID.

Example 2-14: Separating Type and Class Hierarchies
type
  IShape = interface
  ['{50F6D851-F4EB-11D2-88AC-00104BCAC44B}']
    procedure Draw(Canvas: TCanvas);
    function GetPosition: TPoint;
    procedure SetPosition(Value: TPoint);
    property Position: TPoint read GetPosition write SetPosition;
  end;
 
  IPolygon = interface(IShape)
  ['{50F6D852-F4EB-11D2-88AC-00104BCAC44B}']
    function NumVertices: Integer;
    function NumSides: Integer;
    function SideLength(Index: Integer): Integer;
    function Vertex(Index: Integer): TPoint;
  end;
  IRectangle = interface(IPolygon)
  ['{50F6D853-F4EB-11D2-88AC-00104BCAC44B}']
  end;
  ISquare = interface(IRectangle)
  ['{50F6D854-F4EB-11D2-88AC-00104BCAC44B}']
    function Side: Integer;
  end;
 
  TBaseShape = class(TNoRefCount, IShape)
  private
    fPosition: TPoint;
    function GetPosition: TPoint;
    procedure SetPosition(Value: TPoint);
  public
    constructor Create; virtual;
    procedure Draw(Canvas: TCanvas); virtual; abstract;
    property Position: TPoint read fPosition write SetPosition;
  end;
  TPolygon = class(TBaseShape, IPolygon)
  private
    fVertices: array of TPoint;
  public
    procedure Draw(Canvas: TCanvas); override;
    function NumVertices: Integer;
    function NumSides: Integer;
    function SideLength(Index: Integer): Integer;
    function Vertex(Index: Integer): TPoint;
  end;
  TRectangle = class(TBaseShape, IPolygon, IRectangle)
  private
    fRect: TRect;
  public
    procedure Draw(Canvas: TCanvas); override;
    function NumVertices: Integer;
    function NumSides: Integer;
    function SideLength(Index: Integer): Integer;
    function Vertex(Index: Integer): TPoint;
  end;
  TSquare = class(TBaseShape, IPolygon, IRectangle, ISquare)
  private
    fSide: Integer;
  public
    procedure Draw(Canvas: TCanvas); override;
    function Side: Integer;
    function NumVertices: Integer;
    function NumSides: Integer;
    function SideLength(Index: Integer): Integer;
    function Vertex(Index: Integer): TPoint;
  end;

A derived class inherits the interfaces implemented by the ancestors' classes. Thus, TRectangle inherits from TBaseShape, and TBaseShape implements IShape so TRectangle implements IShape. Inheritance of interfaces works a little differently. Interface inheritance is merely a typing convenience, so you don't have to retype a lot of method declarations. When a class implements an interface, that does not automatically mean the class implements the ancestor interfaces. A class implements only those interfaces that are listed in its class declaration (and in the declaration for ancestor classes). Thus, even though IRectangle inherits from IPolygon, the TRectangle class must list IRectangle and IPolygon explicitly.

To implement a type hierarchy, you might not want to use reference counting. Instead, you will rely on explicit memory management, the way you do for normal Delphi objects. In this case, it's best to implement the _AddRef and _Release methods as stubs, such as those in the TNoRefCount class in Example 2-13. Just be careful not to have any variables that hold stale references. A variable that refers to an object that has been freed can cause problems if you use the variable. An interface variable that refers to an object that has been freed will certainly cause problems, because Delphi will automatically call its _Release method. In other words, you never want to have variables that contain invalid pointers, and working with interfaces that do not use reference counting forces you to behave.

COM and Corba

Delphi interfaces are also useful for implementing and using COM and Corba objects. You can define a COM server that implements many interfaces, and Delphi automatically manages the COM aggregation for you. The runtime library contains many classes that make it easier to define COM servers, class factories, and so on. Because these classes are not part of the Delphi Pascal language, they are not covered in this book. Consult the product documentation to learn more.

Reference Counting

The previous section discusses how Delphi uses reference counting to manage the lifetime of interfaces. Strings and dynamic arrays also use reference counting to manage their lifetimes. The compiler generates appropriate code to keep track of when interface references, strings, and dynamic arrays are created and when the variables go out of scope and the objects, strings, and arrays must be destroyed.

Usually, the compiler can handle the reference counting automatically, and everything works the way the you expect it to. Sometimes, though, you need to give a hint to the compiler. For example, if you declare a record that contains a reference counted field, and you use GetMem to allocate a new instance of the record, you must call Initialize, passing the record as an argument. Before calling FreeMem, you must call Finalize.

Sometimes, you want to keep a reference to a string or interface after the variable goes out of scope, that is, at the end of the block where the variable is declared. For example, maybe you want to associate an interface with each item in a TListView. You can do this by explicitly managing the reference count. When storing the interface, be sure to cast it to IUnknown, call _AddRef, and cast the IUnknown reference to a raw pointer. When extracting the data, type cast the pointer to IUnknown. You can then use the as operator to cast the interface to any desired type, or just let Delphi release the interface. For convenience, declare a couple of subroutines to do the dirty work for you, and you can reuse these subroutines any time you need to retain an interface reference. Example 2-15 shows an example of how you can store an interface reference as the data associated with a list view item.

Example 2-15: Storing Interfaces in a List View
// Cast an interface to a Pointer such that the reference
// count is incremented and the interface will not be freed
// until you call ReleaseIUnknown.
function RefIUnknown(const Intf: IUnknown): Pointer;
begin
  Intf._AddRef;               // Increment the reference count.
  Result := Pointer(Intf);    // Save the interface pointer.
end;
 
// Release the interface whose value is stored in the pointer P.
procedure ReleaseIUnknown(P: Pointer);
var
  Intf: IUnknown;
begin
  Pointer(Intf) := P;
  // Delphi releases the interface when Intf goes out of scope.
end;
 
// When the user clicks the button, add an interface to the list.
procedure TForm1.Button1Click(Sender: TObject);
var
  Item: TListItem;
begin
  Item := ListView1.Items.Add;
  Item.Caption := 'Stuff';
  Item.Data := RefIUnknown(GetIntf as IUnknown);
end;
 
// When the list view is destroyed or the list item is destroyed
// for any other reason, release the interface, too.
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
   ReleaseIUnknown(Item.Data);
end;
 
// When the user selects the list view item, do something with the
// associated interface.
procedure TForm1.ListView1Click(Sender: TObject);
var
  Intf: IMyInterface;
begin
  Intf := IUnknown(ListView1.Selected.Data) as IMyInterface;
  Intf.DoSomethingUseful;
end;

You can also store strings as data. Instead of using _AddRef, cast the string to a Pointer to store the reference to the string, then force the variable to forget about the string. When the variable goes out of scope, Delphi will not free the string, because the variable has forgotten all about it. After retrieving the pointer, assign it to a string variable that is cast to a pointer. When the subroutine returns, Delphi automatically frees the string's memory. Be sure your program does not retain any pointers to memory that is about to be freed. Again, convenience subroutines simplify the task. Example 2-16 shows one way to store strings.

Example 2-16: Storing Strings in a List View
// Save a reference to a string and return a raw pointer
// to the string.
function RefString(const S: string): Pointer;
var
  Local: string;
begin
  Local := S;                // Increment the reference count.
  Result := Pointer(Local);  // Save the string pointer.
  Pointer(Local) := nil;     // Prevent decrementing the ref count.
end;
 
// Release a string that was referenced with RefString.
procedure ReleaseString(P: Pointer);
var
  Local: string;
begin
  Pointer(Local) := P;
  // Delphi frees the string when Local goes out of scope.
end;
 
// When the user clicks the button, add an item to the list view
// and save an additional, hidden string.
procedure TForm1.Button1Click(Sender: TObject);
var
  Item: TListItem;
begin
  Item := ListView1.Items.Add;
  Item.Caption := Edit1.Text;
  Item.Data := RefString(Edit2.Text);
end;
 
// Release the string when the list view item is destroyed
// for any reason.
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
  ReleaseString(Item.Data);
end;
 
// Retrieve the string when the user selects the list view item.
procedure TForm1.ListView1Click(Sender: TObject);
var
  Str: string;
begin
  if ListView1.Selected <> nil then
  begin
    Str := string(ListView1.Selected.Data);
    ShowMessage(Str);
  end;
end;

Messages

You should be familiar with Windows messages: user interactions and other events generate messages, which Windows sends to an application. An application processes messages one at a time to respond to the user and other events. Each kind of message has a unique number and two integer parameters. Sometimes a parameter is actually a pointer to a string or structure that contains more complex information. Messages form the heart of Windows event-driven architecture, and Delphi has a unique way of supporting Windows messages.

In Delphi, every object--not only window controls--can respond to messages. A message has an integer identifier and can contain any amount of additional information. In the VCL, the Application object receives Windows messages and maps them to equivalent Delphi messages. In other words, Windows messages are a special case of more general Delphi messages.

A Delphi message is a record where the first two bytes contain an integer message identifier, and the remainder of the record is programmer-defined. Delphi's message dispatcher never refers to any part of the message record past the message number, so you are free to store any amount or kind of information in a message record. By convention, the VCL always uses Windows-style message records (TMessage), but if you find other uses for Delphi messages, you don't need to feel so constrained.

To send a message to an object, fill in the message identifier and the rest of the message record and call the object's Dispatch method. Delphi looks up the message number in the object's message table. The message table contains pointers to all the message handlers that the class defines. If the class does not define a message handler for the message number, Delphi searches the parent class's message table. The search continues until Delphi finds a message handler or it reaches the TObject class. If the class and its ancestor classes do not define a message handler for the message number, Delphi calls the object's DefaultHandler method. Window controls in the VCL override DefaultHandler to pass the message to the window procedure; other classes usually ignore unknown messages. You can override DefaultHandler to do anything you want, perhaps raise an exception.

Use the message directive to declare a message handler for any message. See Chapter 5 for details about the message directive.

Message handlers use the same message table and dispatcher as dynamic methods. Each method that you declare with the dynamic directive is assigned a 16-bit negative number, which is really a message number. A call to a dynamic method uses the same dispatch code to look up the dynamic method, but if the method is not found, that means the dynamic method is abstract, so Delphi calls AbstractErrorProc to report a call to an abstract method.

Because dynamic methods use negative numbers, you cannot write a message handler for negative message numbers, that is, message numbers with the most-significant bit set to one. This limitation should not cause any problems for normal applications. If you need to define custom messages, you have the entire space above WM_USER ($0F00) available, up to $7FFF. Delphi looks up dynamic methods and messages in the same table using a linear search, so with large message tables, your application will waste time performing method lookups.

Delphi's message system is entirely general purpose, so you might find a creative use for it. Usually, interfaces provide the same capability, but with better performance and increased type-safety.

Memory Management

Delphi manages the memory and lifetime of strings, Variants, dynamic arrays, and interfaces automatically. For all other dynamically allocated memory, you--the programmer--are in charge. It's easy to be confused because it seems as though Delphi automatically manages the memory of components, too, but that's just a trick of the VCL.

Components Versus Objects

The VCL's TComponent class has two fancy mechanisms for managing object lifetimes, and they often confuse new Delphi programmers, tricking them into thinking that Delphi always manages object lifetimes. It's important that you understand exactly how components work, so you won't be fooled.

Every component has an owner. When the owner is freed, it automatically frees the components that it owns. A form owns the components you drop on it, so when the form is freed, it automatically frees all the components on the form. Thus, you don't usually need to be concerned with managing the lifetime of forms and components.

When a form or component frees a component it owns, the owner also checks whether it has a published field of the same name as the component. If so, the owner sets that field to nil. Thus, if your form dynamically adds or removes components, the form's fields always contain valid object references or are nil. Don't be fooled into thinking that Delphi does this for any other field or object reference. The trick works only for published fields (such as those automatically created when you drop a component on a form in the IDE's form editor), and only when the field name matches the component name.

Memory management is thread-safe, provided you use Delphi's classes or functions to create the threads. If you go straight to the Windows API and the CreateThread function, you must set the IsMultiThread variable to True. For more information, see Chapter 4, Concurrent Programming.

Ordinarily, when you construct an object, Delphi calls NewInstance to allocate and initialize the object. You can override NewInstance to change the way Delphi allocates memory for the object. For example, suppose you have an application that frequently uses doubly linked lists. Instead of using the general-purpose memory allocator for every node, it's much faster to keep a chain of available nodes for reuse. Use Delphi's memory manager only when the node list is empty. If your application frequently allocates and frees nodes, this special-purpose allocator can be faster than the general-purpose allocator. Example 2-17 shows a simple implementation of this scheme. (See Chapter 4 for a thread-safe version of this class.)

Example 2-17: Custom Memory Management for Linked Lists
type
  TNode = class
  private
    fNext, fPrevious: TNode;
  protected
    // Nodes are under control of TLinkedList.
    procedure Relink(NewNext, NewPrevious: TNode);
    constructor Create(Next: TNode = nil; Previous: TNode = nil);
    procedure RealFree;
 
  public
    destructor Destroy; override;
    class function NewInstance: TObject; override;
    procedure FreeInstance; override;
    property Next: TNode read fNext;
    property Previous: TNode read fPrevious;
end;
 
// Singly linked list of nodes that are free for reuse.
// Only the Next fields are used to maintain this list.
var
  NodeList: TNode;
 
// Allocate a new node by getting the head of the NodeList.
// Remember to call InitInstance to initialize the node that was
// taken from NodeList.
// If the NodeList is empty, allocate a node normally.
class function TNode.NewInstance: TObject;
begin
  if NodeList = nil then
    Result := inherited NewInstance
  else
  begin
    Result := NodeList;
    NodeList := NodeList.Next;
    InitInstance(Result);
  end;
end;
 
// Because the NodeList uses only the Next field, set the Previous
// field to a special value. If a program erroneously refers to the
// Previous field of a free node, you can see the special value
// and know the cause of the error.
const
  BadPointerValueToFlagErrors = Pointer($F0EE0BAD);
 
// Free a node by adding it to the head of the NodeList. This is MUCH
// faster than using the general-purpose memory manager.
procedure TNode.FreeInstance;
begin
  fPrevious := BadPointerValueToFlagErrors;
  fNext := NodeList;
  NodeList := Self;
end;
 
// If you want to clean up the list properly when the application
// finishes, call RealFree for each node in the list. The inherited
// FreeInstance method frees and cleans up the node for real.
procedure TNode.RealFree;
begin
  inherited FreeInstance;
end;

You can also replace the entire memory management system that Delphi uses. Install a new memory manager by calling SetMemoryManager. For example, you might want to replace Delphi's suballocator with an allocator that performs additional error checking. Example 2-18 shows a custom memory manager that keeps a list of pointers the program has allocated and explicitly checks each attempt to free a pointer against the list. Any attempt to free an invalid pointer is refused, and Delphi will report a runtime error (which SysUtils changes to an exception). As a bonus, the memory manager checks that the list is empty when the application ends. If the list is not empty, you have a memory leak.

Example 2-18: Installing a Custom Memory Manager
unit CheckMemMgr;
 
interface
 
uses Windows;
 
function CheckGet(Size: Integer): Pointer;
function CheckFree(Mem: Pointer): Integer;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
 
var
  HeapFlags: DWord; // In a single-threaded application, you might
                    // want to set this to Heap_No_Serialize.
implementation
 
const
  MaxSize = MaxInt div 4;
type
  TPointerArray = array[1..MaxSize] of Pointer;
  PPointerArray = ^TPointerArray;
var
  Heap: THandle;             // Windows heap for the pointer list
  List: PPointerArray;       // List of allocated pointers
  ListSize: Integer;         // Number of pointers in the list
  ListAlloc: Integer;        // Capacity of the pointer list
 
// If the list of allocated pointers is not empty when the program
// finishes, that means you have a memory leak. Handling the memory
// leak is left as an exercise for the reader.
procedure MemoryLeak;
begin
  // Report the leak to the user, but remember that the program is
  // shutting down, so you should probably stick to the Windows API
  // and not use the VCL.
end;
 
// Add a pointer to the list.
procedure AddMem(Mem: Pointer);
begin
  if List = nil then
  begin
    // New list of pointers.
    ListAlloc := 8;
    List := HeapAlloc(Heap, HeapFlags, ListAlloc * SizeOf(Pointer));
  end
  else if ListSize >= ListAlloc then
  begin
    // Make the list bigger. Try to do it somewhat intelligently.
    if ListAlloc < 256 then
      ListAlloc := ListAlloc * 2
    else
      ListAlloc := ListAlloc + 256;
    List := HeapRealloc(Heap, HeapFlags, List,
                        ListAlloc * SizeOf(Pointer));
  end;
  // Add a pointer to the list.
  Inc(ListSize);
  List[ListSize] := Mem;
end;
 
// Look for a pointer in the list, and remove it. Return True for
// success, and False if the pointer is not in the list.
function RemoveMem(Mem: Pointer): Boolean;
var
  I: Integer;
begin
  for I := 1 to ListSize do
    if List[I] = Mem then
    begin
      MoveMemory(@List[I], @List[I+1], (ListSize-I) * SizeOf(Pointer));
      Dec(ListSize);
      Result := True;
      Exit;
    end;
 
  Result := False;
end;
 
// Replacement memory allocator.
function CheckGet(Size: Integer): Pointer;
begin
  Result := SysGetMem(Size);
  AddMem(Result);
end;
 
// If the pointer isn't in the list, don't call the real
// Free function. Return 0 for success, and non-zero for an error.
function CheckFree(Mem: Pointer): Integer;
begin
  if not RemoveMem(Mem) then
    Result := 1
  else
    Result := SysFreeMem(Mem);
end;
 
// Remove the old pointer and add the new one, which might be the
// same as the old one, or it might be different. Return nil for
// an error, and Delphi will raise an exception.
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
begin
  if not RemoveMem(Mem) then
    Result := nil
  else
  begin
    Result :=SysReallocMem(Mem, Size);
    AddMem(Result);
  end;
end;
 
procedure SetNewManager;
var
  Mgr: TMemoryManager;
begin
  Mgr.GetMem := CheckGet;
  Mgr.FreeMem := CheckFree;
  Mgr.ReallocMem := CheckRealloc;
  SetMemoryManager(Mgr);
end;
 
initialization
  Heap := HeapCreate(0, HeapFlags, 0);
  SetNewManager;
finalization
  if ListSize <> 0 then
    MemoryLeak;
  HeapDestroy(Heap);
end.

If you define a custom memory manager, you must ensure that your memory manager is used for all memory allocation. The easiest way to do this is to set the memory manager in a unit's initialization section, as shown in Example 2-18. The memory management unit must be the first unit listed in the project's uses declaration.

Ordinarily, if a unit makes global changes in its initialization section, it should clean up those changes in its finalization section. A unit in a package might be loaded and unloaded many times in a single application, so cleaning up is important. A memory manager is different, though. Memory allocated by one manager cannot be freed by another manager, so you must ensure that only one manager is active in an application, and that the manager is active for the entire duration of the application. This means you must not put your memory manager in a package, although you can use a DLL, as explained in the next section.

Memory and DLLs

If you use DLLs and try to pass objects between DLLs or between the application and a DLL, you run into a number of problems. First of all, each DLL and EXE keeps its own copy of its class tables. The is and as operators do not work correctly for objects passed between DLLs and EXEs. Use packages (described in Chapter 1) to solve this problem. Another problem is that any memory allocated in a DLL is owned by that DLL. When Windows unloads the DLL, all memory allocated by the DLL is freed, even if the EXE or another DLL holds a pointer to that memory. This can be a major problem when using strings, dynamic arrays, and Variants because you never know when Delphi will allocate memory automatically.

The solution is to use the ShareMem unit as the first unit of your project and every DLL. The ShareMem unit installs a custom memory manager that redirects all memory allocation requests to a special DLL, BorlndMM.dll. The application doesn't unload BorlndMM until the application exits. The DLL magic takes place transparently, so you don't need to worry about the details. Just make sure you use the ShareMem unit, and make sure it is the first unit used by your program and libraries. When you release your application to your clients or customers, you will need to include BorlndMM.dll.

If you define your own memory manager, and you need to use DLLs, you must duplicate the magic performed by the ShareMem unit. You can replace ShareMem with your own unit that forwards memory requests to your DLL, which uses your custom memory manager. Example 2-19 shows one way to define your own replacement for the ShareMem unit.

Example 2-19: Defining a Shared Memory Manager
unit CheckShareMem;
 
// Use this unit first so all memory allocations use the shared
// memory manager. The application and all DLLs must use this unit.
// You cannot use packages because those DLLs use the default Borland
// shared memory manager.
 
interface
 
function CheckGet(Size: Integer): Pointer;
function CheckFree(Mem: Pointer): Integer;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
 
implementation
 
const
  DLL = 'CheckMM.dll';
 
function CheckGet(Size: Integer): Pointer; external DLL;
function CheckFree(Mem: Pointer): Integer; external DLL;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
    external DLL;
 
procedure SetNewManager;
var
  Mgr: TMemoryManager;
begin
  Mgr.GetMem := CheckGet;
  Mgr.FreeMem := CheckFree;
  Mgr.ReallocMem := CheckRealloc;
  SetMemoryManager(Mgr);
end;
 
initialization
  SetNewManager;
end.

The CheckMM DLL uses your custom memory manager and exports its functions so they can be used by the CheckShareMem unit. Example 2-20 shows the source code for the CheckMM library.

Example 2-20: Defining the Shared Memory Manager DLL
library CheckMM;
 
// Replacement for BorlndMM.dll to use a custom memory manager.
 
uses
  CheckMemMgr;
 
exports
  CheckGet, CheckFree, CheckRealloc;
 
begin
end.

Your program and library projects use the CheckShareMem unit first, and all memory requests go to CheckMM.dll, which uses the error-checking memory manager. You don't often need to replace Delphi's memory manager, but as you can see, it isn't difficult to do.

TIP:  

The memory manager that comes with Delphi works well for most applications, but it does not perform well in some cases. The average application allocates and frees memory in chunks of varying sizes. If your application is different and allocates memory in ever-increasing sizes (say, because you have a dynamic array that grows in small steps to a very large size), performance will suffer. Delphi's memory manager will allocate more memory than your application needs. One solution is to redesign your program so it uses memory in a different pattern (say, by preallocating a large dynamic array). Another solution is to write a memory manager that better meets the specialized needs of your application. For example, the new memory manager might use the Windows API (HeapAllocate, etc.).

Old-Style Object Types

In addition to class types, Delphi supports an obsolete type that uses the object keyword. Old-style objects exist for backward compatibility with Turbo Pascal, but they might be dropped entirely from future versions of Delphi.

Old-style object types are more like records than new-style objects. Fields in an old-style object are laid out in the same manner as in records. If the object type does not have any virtual methods, there is no hidden field for the VMT pointer, for example. Unlike records, object types can use inheritance. Derived fields appear after inherited fields. If a class declares a virtual method, its first field is the VMT pointer, which appears after all the inherited fields. (Unlike a new-style object, where the VMT pointer is always first because TObject declares virtual methods.)

An old-style object type can have private, protected, and public sections, but not published or automated sections. Because it cannot have a published section, an old object type cannot have any runtime type information. An old object type cannot implement interfaces.

Constructors and destructors work differently in old-style object types than in new-style class types. To create an instance of an old object type, call the New procedure. The newly allocated object is initialized to all zero. If you declare a constructor, you can call it as part of the call to New. Pass the constructor name and arguments as the second argument to New. Similarly, you can call a destructor when you call Dispose to free the object instance. The destructor name and arguments are the second argument to Dispose.

You don't have to allocate an old-style object instance dynamically. You can treat the object type as a record type and declare object-type variables as unit-level or local variables. Delphi automatically initializes string, dynamic array, and Variant fields, but does not initialize other fields in the object instance.

Unlike new-style class types, exceptions in old-style constructors do not automatically cause Delphi to free a dynamically created object or call the destructor.


1. C++ can emulate interfaces with abstract classes.

2. Interfaces use reference counting to manage lifetimes.

3. RTTI in C++ is limited to comparing and casting types.

4. A built-in assembler is not part of the C++ language standard, but most C++ compilers, including Borland's, support a built-in assembler as a language extension.

Back to: Delphi in a Nutshell


O'Reilly Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com