|
|
|
|
Delphi in a NutshellBy Ray Lischner1st Edition March 2000 1-56592-659-5, Order Number: 6595 600 pages, $29.95 |
Chapter 2
The Delphi Object ModelDelphi'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 theendkeyword. 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 typeTAccount = classprivatefCustomer: string; // name of customerfNumber: Cardinal; // account numberfBalance: Currency; // current account balanceend;TSavingsAccount = class(TAccount)privatefInterestRate: Integer; // annual percentage rate, scaled by 1000end;TCheckingAccount = class(TAccount)privatefReturnChecks: Boolean;end;TCertificateOfDeposit = class(TSavingsAccount)privatefTerm: Cardinal; // CD maturation term, in daysend;varCD1, CD2: TAccount;beginCD1 := 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
Single root class
Metaclasses
Class (static) fields
Virtual methods
Abstract (pure) virtual methods
Class (static) methods
Dynamic methods
Garbage collection
Varianttypes
OLE automation
Static type-checking
Exception handling
Function overloading
Operator overloading
Non-class functions
Non-object variables
Properties
Runtime type information
Generic types (templates)
Built-in support for threads
Message passing
Built-in assembler
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
isoperator) or to cast an object to a particular type (with theasoperator). 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 typeTComplexClass = class of TComplex; // metaclass typeTComplex = class(TPersistent)privatefReal, fImaginary: Double;publicconstructor Create(Re: Double = 0.0); overload;constructor Create(Re, Im: Double); overload;destructor Destroy; override;procedure Assign(Source: TPersistent); override;function AsString: string;publishedproperty 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 withCreatefor all your overloaded constructors.To get rid of the object when your program no longer needs it, call the
Freemethod. To ensure that the object is properly freed, even if an exception is raised, use atry-finallyexception handler. (See Chapter 1, Delphi Pascal, for more information abouttry-finally.) For example:Obj := TSomeOtherClass.Create;tryObj.DoSomethingThatMightRaiseAnException;Obj.DoSomethingElse;finallyObj.Free;end;When freeing a global variable or field, always set the variable to
nilwhen freeing the object so you are not left with a variable that contains an invalid pointer. You should take care to set the variable tonilbefore freeing the object. If the destructor, or a method called from the destructor, refers to that variable, you usually want the variable to benilto avoid any potential problems. An easy way to do this is to call theFreeAndNilprocedure (from theSysUtilsunit):GlobalVar := TFruitWigglies.Create;tryGlobalVar.EatEmUp;finallyFreeAndNil(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 usesTObjectas 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,TCertificateOfDeposithas a base class ofTSavingsAccount; its ancestor classes areTObject,TAccount, andTSavingsAccount.The
TObjectclass 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 theTObjectclass 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
isoperator 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 isnilor of the wrong type. For example:if Account is TCheckingAccount then ... // tests the class of Accountif Account is TObject then ... // True when Account is not nilYou 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
asoperator for type casts. Theasoperator automatically checks the object's type and raises a runtime error if the object's class is not a descendant of the target class. (TheSysUtilsunit maps the runtime error to anEInvalidCastexception.)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 varAccount: TAccount;Checking: TCheckingAccount;beginAccount := Checking; // AllowedChecking := Account; // Compile-time errorChecking := Account as TCheckingAccount; // OkayAccount as TForm; // Raises a runtime errorChecking := TCheckingAccount(Account); // Okay, but not recommendedif Account is TCheckingAccount then // BetterChecking := TCheckingAccount(Account)elseChecking := 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, andVariants are set toUnassigned. (SeeNewInstanceandInitInstancein 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.Selfis similar tothisin 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,
Selfrefers 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 typeTAccount = classpublicprocedure Withdraw(Amount: Currency);end;TSavingsAccount = class(TAccount)publicprocedure Withdraw(Amount: Currency);end;varSavings: TSavingsAccount;Account: TAccount;begin...Savings.Withdraw(1000.00); // Calls TSavingsAccount.WithdrawAccount.Withdraw(1000.00); // Calls TAccount.WithdrawAn 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
virtualdirective in the base class, and use theoverridedirective to provide a new definition of the method in a derived class. Unlike in Java, methods are static by default, and you must use thevirtualdirective to declare a virtual method. Unlike in C++, you must use theoverridedirective to override a virtual method in a derived class.Example 2-5 uses virtual methods.
Example 2-5: Binding Virtual Methods typeTAccount = classpublicprocedure Withdraw(Amount: Currency); virtual;end;TSavingsAccount = class(TAccount)publicprocedure Withdraw(Amount: Currency); override;end;varSavings: TSavingsAccount;Account: TAccount;begin...Savings.Withdraw(1000.00); // Calls TSavingsAccount.WithdrawAccount := Savings;Account.Withdraw(1000.00); // Calls TSavingsAccount.WithdrawInstead of using the
virtualdirective, you can also use thedynamicdirective. 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 untilTObjectis 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
abstractdirective, 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
overrideandabstractdirectives. 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
overloaddirective. A derived class can overload a method it inherits from a base class. In that case, only the derived class needs theoverloaddirective. 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 typeTAuditKind = (auInternal, auExternal, auIRS, auNasty);TAccount = classpublicprocedure Audit;end;TCheckingAccount = class(TAccount)publicprocedure Audit(Kind: TAuditKind); // Hides TAccount.Auditend;TSavingsAccount = class(TAccount)public// Can call TSavingsAccount.Audit and TAccount.Auditprocedure Audit(Kind: TAuditKind); overload;end;varChecking: TCheckingAccount;Savings: TSavingsAccount;beginChecking := TCheckingAccount.Create;Savings := TSavingsAccount.Create;Checking.Audit; // Error because TAccount.Audit is hiddenSavings.Audit; // Okay because Audit is overloadedSavings.Audit(auNasty); // OkayChecking.Audit(auInternal); // OkayConstructors
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 withCreate, but convey additional information, such asCreateFromFileorCreateFromStream. Usually, though, the simple nameCreateis sufficient, and you can use method overloading to define multiple constructors with the same name. Another reason to overload the nameCreateis 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
NewInstancemethod to allocate a new instance of the class. After callingNewInstance, the constructor continues and initializes the object. The constructor automatically sets up atry-exceptblock, 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-exceptblock and does not callNewInstance. 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; // rightOne 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
AfterConstructionmethod and use that for any code that needs to wait until the object is fully constructed. If you overrideAfterConstruction, 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 typeTCustomer = class ... end;TAccount = classprivatefBalance: Currency;fNumber: Cardinal;fCustomer: TCustomer;publicconstructor Create(Customer: TCustomer); virtual;destructor Destroy; override;end;TSavingsAccount = class(TAccount)privatefInterestRate: Integer; // Scaled by 1000publicconstructor 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;varAccountNumber: Cardinal = 1;constructor TAccount.Create(Customer: TCustomer);begininherited 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 thenCustomer.ReleaseAccount(Self);// Call TObject.Destroy.inherited Destroy;end;constDefaultInterestRate = 5000; // 5%, scaled by 1000constructor 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
FreeInstanceto 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 overrideBeforeDestructionto 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
Destroydestructor, but you must not redeclare theFreemethod. When freeing an object, you should call theFreemethod and not the destructor. The distinction is important, becauseFreechecks whether the object reference isniland callsDestroyonly for non-nilreferences. In extraordinary circumstances, a class can redefine theFreemethod (such asTInterfacein the seldom-usedVirtIntfunit), which makes it that much more important to callFree, notDestroy.If a constructor or
AfterConstructionmethod 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 theFreemethod andFreeMemprocedure both check fornilpointers. If the destructor calls other methods, though, always check first for anilpointer.Object Life Cycle
For most objects, you call a constructor to create the object, use the object, and then call
Freeto 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 typeTSomething = classprocedure DoSomething;end;varRef: TSomething;beginRef := TSomething.Create;Ref.DoSomething;Ref.Free;end;// The hidden code in the constructor looks something like this:function TSomething.Create(IsClassRef: Boolean): TSomething;beginif IsClassRef thentry// 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;endelseSelf.Create(False);Result := Self;end;// The hidden code in the destructor looks something like this:procedure TSomething.Destroy(Deallocate: Boolean);beginif Deallocate thenSelf.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 thenbegin// 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
TForminherits fromTPersistent, 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.
typeTPublic = class;TPrivateHelper = classprivate// 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 = classprivatefHelper: TPrivateHelper;publicconstructor Create;destructor Destroy;end;constructor TPrivateHelper.Create;beginraise 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
TRecttype, but because it is a class, it has properties and methods.
Example 2-9: Properties Readers and Writers TRectEx = class(TPersistent)privateR: TRect;function GetHeight: Integer;function GetWidth: Integer;procedure SetHeight(const Value: Integer);procedure SetWidth(const Value: Integer);publicconstructor 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;publishedproperty 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 typeTExample = class...property Items[I: Integer]: Integer read GetItem write SetItem;property Chars[C: Char]: Char read GetChar write SetChar; default;end;varExample: TExample;I: Integer;C: Char;beginExample := TExample.Create;I := Example.Items[4]; // Must mention property name explicitlyC := Example['X']; // Array property is defaultC := Example.Chars['X']; // Same as previous lineIndexed 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
storedanddefaultdirectives. 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 thestoreddirective is a Boolean constant, a field of Boolean type, or a method that takes no arguments and returns a Boolean result. The value for thedefaultdirective is a constant value of the same type as the property. Only enumerated, integer, and set-type properties can have a default value. Thestoredanddefaultdirectives 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
defaultdirective 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 typeTCustomer = recordName: string;TaxIDNumber: string[9];end;TAccount = classprivatefCustomer: TCustomer;fBalance: Currency;fNumber: Cardinal;procedure SetBalance(NewBalance: Currency);publishedproperty Balance: Currency read fBalance write SetBalance;property Number: Cardinal read fNumber; // Cannot change account #property CustName: string read fCustomer.Name;end;TSavingsAccount = class(TAccount)privatefInterestRate: Integer;publishedproperty InterestRate: Integer read fInterestRatewrite fInterestRate default DefaultInterestRate;end;TLinkedAccount = class(TObject)privatefAccounts: 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 = classprivatefList: TList;function GetAccount(Index: Integer): TAccount;procedure SetAccount(Index: Integer; Account: TAccount);function GetCount: Integer;protectedproperty List: TList read fList;publicproperty Count: Integer read GetCount;property Accounts[Index: Integer]: TAccount read GetAccountwrite SetAccount; default;end;procedure TAccount.SetBalance(NewBalance: Currency);beginif NewBalance < 0 thenraise EOverdrawnException.Create;fBalance := NewBalance;end;function TLinkedAccount.GetAccount(Index: Integer): TAccount;beginResult := fAccounts[Index]end;function TAccountList.GetCount: Integer;beginResult := List.Countend;function TAccountList.GetAccount(Index: Integer): TAccount;beginResult := List[Index]end;procedure TAccountList.SetAccount(Index: Integer; Account: TAccount);beginfList[Index] := Accountend;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
TPersistentand that the class overrides theAssignmethod. Implement your property's write method to callAssign. (TPersistent--in theClassesunit--is not required, but it's the easiest way to copy an object. Otherwise, you need to duplicate theAssignmethod in whatever class you use.) The read method can provide direct access to the field. If the property object has anOnChangeevent, 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. TheBitmapproperty stores aTBitmapobject.Example 2-12: Declaring and Using a Class-type Property unit Tile;interfaceuses SysUtils, Classes, Controls, Graphics;type// Tile a bitmapTTile = class(TGraphicControl)privatefBitmap: TBitmap;procedure SetBitmap(NewBitmap: TBitmap);procedure BitmapChanged(Sender: TObject);protectedprocedure Paint; override;publicconstructor Create(Owner: TComponent); override;destructor Destroy; override;publishedproperty 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);begininherited;fBitmap := TBitmap.Create;fBitmap.OnChange := BitmapChanged;end;// Free the bitmap when destroying the control.destructor TTile.Destroy;beginFreeAndNil(fBitmap);inherited;end;// When the bitmap changes, redraw the control.procedure TTile.BitmapChanged(Sender: TObject);beginInvalidate;end;// Paint the control by tiling the bitmap. If there is no// bitmap, don't paint anything.procedure TTile.Paint;varX, Y: Integer;beginif (Bitmap.Width = 0) or (Bitmap.Height = 0) thenExit;Y := 0;while Y < ClientHeight dobeginX := 0;while X < ClientWidth dobeginCanvas.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);beginfBitmap.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 fromIUnknown. TheIUnknowninterface declares three methods:_AddRef,_Release, andQueryInterface. 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, andQueryInterfacemethods is to inherit them fromTInterfacedObjector 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
classkeyword in Chapter 5 for more information about redirecting methods.A class can delegate the implementation of an interface to a property that uses the
implementsdirective. 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 theimplementsdirective 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
_AddRefand_Releaseto 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
_AddRefand_Releaseis entirely up to you. If you inherit fromTInterfacedObject, these methods implement reference counting. The_AddRefmethod increments the reference count, and_Releasedecrements it. When the reference count goes to zero,_Releasefrees the object. If you inherit from a different class, you can define these methods to do anything you want. You should implementQueryInterfacecorrectly, though, because Delphi relies on it to implement theasoperator.Typecasting
Delphi calls
QueryInterfaceas part of its implementation of theasoperator for interfaces. You can use theasoperator to cast an interface to any other interface type. Delphi callsQueryInterfaceto obtain the new interface reference. IfQueryInterfacereturns an error, theasoperator raises a runtime error. (TheSysUtilsunit maps the runtime error to anEIntfCastErrorexception.)You can implement
QueryInterfaceany way you want, but you probably want to use the same approach taken byTInterfacedObject. Example 2-13 shows a class that implementsQueryInterfacenormally, but uses stubs for_AddRefand_Release. Later in this section, you'll see how useful this class can be.Example 2-13: Interface Base Class Without Reference Counting typeTNoRefCount = class(TObject, IUnknown)protectedfunction 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;beginif GetInterface(IID, Obj) thenResult := 0elseResult := Windows.E_NoInterface;end;function TNoRefCount._AddRef: Integer;beginResult := -1end;function TNoRefCount._Release: Integer;beginResult := -1end;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'sisoperator, where you test whether anAccountvariableis 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
TSquarefromTRectangle. A rectangle is a polygon, but you probably don't want to deriveTRectanglefromTPolygon. 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. ATSquareobject can get away with storing a single length for all of its sides. ATRectangleobject, however, must store two lengths. ATPolygonobject 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,
ISquareinherits fromIRectangle, which inherits fromIPolygon. The interfaces follow the "is-a" relationship. Entirely separate from the interfaces, the classTSquareimplementsISquare,IRectangle, andIPolygon.TRectangleimplementsIRectangleandIPolygon.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,
TBaseShapeimplements the common methods and fields for all shapes.TRectangleinherits fromTBaseShapeand implements the methods in a way that make sense for rectangles.TPolygonalso inherits fromTBaseShapeand implements the methods in a way that make sense for other kinds of polygons.A drawing program can use the shapes by manipulating
IPolygoninterfaces. 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 usingQueryInterface. If you need the GUID of an interface (in an explicit call toQueryInterface, 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 typeIShape = 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)privatefPosition: TPoint;function GetPosition: TPoint;procedure SetPosition(Value: TPoint);publicconstructor Create; virtual;procedure Draw(Canvas: TCanvas); virtual; abstract;property Position: TPoint read fPosition write SetPosition;end;TPolygon = class(TBaseShape, IPolygon)privatefVertices: array of TPoint;publicprocedure 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)privatefRect: TRect;publicprocedure 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)privatefSide: Integer;publicprocedure 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,
TRectangleinherits fromTBaseShape, andTBaseShapeimplementsIShapesoTRectangleimplementsIShape. 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 thoughIRectangleinherits fromIPolygon, theTRectangleclass must listIRectangleandIPolygonexplicitly.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
_AddRefand_Releasemethods as stubs, such as those in theTNoRefCountclass 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_Releasemethod. 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
GetMemto allocate a new instance of the record, you must callInitialize, passing the record as an argument. Before callingFreeMem, you must callFinalize.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 toIUnknown, call_AddRef, and cast theIUnknownreference to a raw pointer. When extracting the data, type cast the pointer toIUnknown. You can then use theasoperator 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;beginIntf._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);varIntf: IUnknown;beginPointer(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);varItem: TListItem;beginItem := 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);beginReleaseIUnknown(Item.Data);end;// When the user selects the list view item, do something with the// associated interface.procedure TForm1.ListView1Click(Sender: TObject);varIntf: IMyInterface;beginIntf := IUnknown(ListView1.Selected.Data) as IMyInterface;Intf.DoSomethingUseful;end;You can also store strings as data. Instead of using
_AddRef, cast the string to aPointerto 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;varLocal: string;beginLocal := 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);varLocal: string;beginPointer(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);varItem: TListItem;beginItem := 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);beginReleaseString(Item.Data);end;// Retrieve the string when the user selects the list view item.procedure TForm1.ListView1Click(Sender: TObject);varStr: string;beginif ListView1.Selected <> nil thenbeginStr := 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
Applicationobject 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
Dispatchmethod. 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 theTObjectclass. If the class and its ancestor classes do not define a message handler for the message number, Delphi calls the object'sDefaultHandlermethod. Window controls in the VCL overrideDefaultHandlerto pass the message to the window procedure; other classes usually ignore unknown messages. You can overrideDefaultHandlerto do anything you want, perhaps raise an exception.Use the
messagedirective to declare a message handler for any message. See Chapter 5 for details about themessagedirective.Message handlers use the same message table and dispatcher as dynamic methods. Each method that you declare with the
dynamicdirective 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 callsAbstractErrorProcto 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.
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
CreateThreadfunction, you must set theIsMultiThreadvariable toTrue. For more information, see Chapter 4, Concurrent Programming.Ordinarily, when you construct an object, Delphi calls
NewInstanceto allocate and initialize the object. You can overrideNewInstanceto 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 typeTNode = classprivatefNext, fPrevious: TNode;protected// Nodes are under control of TLinkedList.procedure Relink(NewNext, NewPrevious: TNode);constructor Create(Next: TNode = nil; Previous: TNode = nil);procedure RealFree;publicdestructor 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.varNodeList: 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;beginif NodeList = nil thenResult := inherited NewInstanceelsebeginResult := 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.constBadPointerValueToFlagErrors = 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;beginfPrevious := 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;begininherited 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 (whichSysUtilschanges 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;interfaceuses Windows;function CheckGet(Size: Integer): Pointer;function CheckFree(Mem: Pointer): Integer;function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;varHeapFlags: DWord; // In a single-threaded application, you might// want to set this to Heap_No_Serialize.implementationconstMaxSize = MaxInt div 4;typeTPointerArray = array[1..MaxSize] of Pointer;PPointerArray = ^TPointerArray;varHeap: THandle; // Windows heap for the pointer listList: PPointerArray; // List of allocated pointersListSize: Integer; // Number of pointers in the listListAlloc: 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);beginif List = nil thenbegin// New list of pointers.ListAlloc := 8;List := HeapAlloc(Heap, HeapFlags, ListAlloc * SizeOf(Pointer));endelse if ListSize >= ListAlloc thenbegin// Make the list bigger. Try to do it somewhat intelligently.if ListAlloc < 256 thenListAlloc := ListAlloc * 2elseListAlloc := 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;varI: Integer;beginfor I := 1 to ListSize doif List[I] = Mem thenbeginMoveMemory(@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;beginResult := 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;beginif not RemoveMem(Mem) thenResult := 1elseResult := 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;beginif not RemoveMem(Mem) thenResult := nilelsebeginResult :=SysReallocMem(Mem, Size);AddMem(Result);end;end;procedure SetNewManager;varMgr: TMemoryManager;beginMgr.GetMem := CheckGet;Mgr.FreeMem := CheckFree;Mgr.ReallocMem := CheckRealloc;SetMemoryManager(Mgr);end;initializationHeap := HeapCreate(0, HeapFlags, 0);SetNewManager;finalizationif ListSize <> 0 thenMemoryLeak;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
usesdeclaration.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
isandasoperators 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, andVariants because you never know when Delphi will allocate memory automatically.The solution is to use the
ShareMemunit as the first unit of your project and every DLL. TheShareMemunit 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 theShareMemunit, 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
ShareMemunit. You can replaceShareMemwith 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 theShareMemunit.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.interfacefunction CheckGet(Size: Integer): Pointer;function CheckFree(Mem: Pointer): Integer;function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;implementationconstDLL = '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;varMgr: TMemoryManager;beginMgr.GetMem := CheckGet;Mgr.FreeMem := CheckFree;Mgr.ReallocMem := CheckRealloc;SetMemoryManager(Mgr);end;initializationSetNewManager;end.The
CheckMMDLL uses your custom memory manager and exports its functions so they can be used by theCheckShareMemunit. Example 2-20 shows the source code for theCheckMMlibrary.Example 2-20: Defining the Shared Memory Manager DLL library CheckMM;// Replacement for BorlndMM.dll to use a custom memory manager.usesCheckMemMgr;exportsCheckGet, CheckFree, CheckRealloc;beginend.Your program and library projects use the
CheckShareMemunit 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
objectkeyword. Old-style objects exist for backward compatibility with Turbo Pascal, but they might be dropped entirely from future versions of Delphi.Old-style
objecttypes are more like records than new-style objects. Fields in an old-styleobjectare laid out in the same manner as in records. If theobjecttype does not have any virtual methods, there is no hidden field for the VMT pointer, for example. Unlike records,objecttypes 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 becauseTObjectdeclares 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
objecttype cannot have any runtime type information. An oldobjecttype cannot implement interfaces.Constructors and destructors work differently in old-style
objecttypes than in new-styleclasstypes. To create an instance of an oldobjecttype, call theNewprocedure. The newly allocated object is initialized to all zero. If you declare a constructor, you can call it as part of the call toNew. Pass the constructor name and arguments as the second argument toNew. Similarly, you can call a destructor when you callDisposeto free the object instance. The destructor name and arguments are the second argument toDispose.You don't have to allocate an old-style
objectinstance dynamically. You can treat theobjecttype as a record type and declareobject-type variables as unit-level or local variables. Delphi automatically initializes string, dynamic array, andVariantfields, but does not initialize other fields in theobjectinstance.Unlike new-style
classtypes, 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
© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com