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.
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, 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).
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.
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.
Delphi |
Java |
C++ |
Visual Basic | |
Inheritance |
✓ |
✓ |
✓ | |
Multiple inheritance |
✓ | |||
Interfaces |
✓ |
✓ |
[a] |
✓ |
Single root class |
✓ |
✓ | ||
Metaclasses |
✓ |
✓ | ||
Class (static) fields |
✓ |
✓ | ||
Virtual methods |
✓ |
✓ |
✓ | |
Abstract (pure) virtual methods |
✓ |
✓ |
✓ | |
Class (static) methods |
✓ |
✓ |
✓ | |
Dynamic methods |
✓ | |||
Garbage collection |
[b] |
✓ | ||
|
✓ |
✓ | ||
OLE automation |
✓ |
✓ | ||
Static type-checking |
✓ |
✓ |
✓ | |
Exception handling |
✓ |
✓ |
✓ |
✓ |
Function overloading |
✓ |
✓ |
✓ | |
Operator overloading |
✓ | |||
Non-class functions |
✓ |
✓ |
✓ | |
Non-object variables |
✓ |
✓ |
✓ | |
Properties |
✓ |
✓ | ||
Runtime type information |
✓ |
✓ |
[c] | |
Generic types (templates) |
✓ | |||
Built-in support for threads |
✓ |
✓ | ||
Message passing |
✓ | |||
Built-in assembler |
✓ |
[d] | ||
Inline functions |
✓ | |||
[a] C++ can emulate interfaces with abstract classes. [b] Interfaces use reference counting to manage lifetimes. [c] RTTI in C++ is limited to comparing and casting types. [d] 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. |
The following sections explain each of these language features in more detail.
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.
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.
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;
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, 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.
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.
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,
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.
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;
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 Variant
s 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
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.
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.
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.
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.
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
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.
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.
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.
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.
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,
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.
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.
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.
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.
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;
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:
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.
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, for details), the default access level is
public.
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.
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 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.
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.
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;
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.
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
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.
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.
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.
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;
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.
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.
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.
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.
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.
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;
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
.
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.
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.
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.
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.
// 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.
// 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;
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.
Delphi manages the
memory and lifetime of strings, Variant
s, 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.
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.
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.)
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.
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.
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
Variant
s 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.
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.
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.
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.).
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.