The Delphi Object Model (PART II)
Sample Chapter of Delphi in a Nutshell
Product:
Delphi all versions
Category:
Skill Level:
Scoring:
Last Update:
05/21/2000
Search Keys:
delphi delphi3000 article Object Model OOP interfaces
Times Scored:
7
Visits:
5557
Uploader: Stefan Walther
Company: bluestep.com IT-Consulting
Reference: Ray Lischner - O'Reilly
Question/Problem/Abstract:
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.
Answer:
Reprinted with permission from O'Reilly & Associates
Object Life Cycle
For most objects, you call a constructor to create the object, use the object, and then call Free to free the object. Delphi handles all the other details for you. Sometimes, though, you need to know a little more about the inner mechanisms of Delphi's object model. Example 2-8 shows the methods that Delphi calls or simulates when it creates and frees an object.
Example 2-8: The Life Cycle of an Object
type
TSomething = class
procedure DoSomething;
end;
var
Ref: TSomething;
begin
Ref := TSomething.Create;
Ref.DoSomething;
Ref.Free;
end;
// The hidden code in the constructor looks something like this:
function TSomething.Create(IsClassRef: Boolean): TSomething;
begin
if IsClassRef then
try
// Allocate the new object.
Self := TSomething.NewInstance;
// NewInstance initializes the object in the same way that
// InitInstance does. If you override NewInstance, though,
// and do not call the inherited NewInstance, you must call
// InitInstance. The call is shown below, so you know what
// happens, but remember that ordinarily Delphi does not
// actually call InitInstance.
InitInstance(Self);
// Do the real work of the constructor, but without all the
// class reference overhead. Delphi does not really call the
// constructor recursively.
Self.Create(False);
Self.AfterConstruction;
except
// If any exception occurs, Delphi automatically calls the
// object's destructor.
Self.Destroy;
end
else
Self.Create(False);
Result := Self;
end;
// The hidden code in the destructor looks something like this:
procedure TSomething.Destroy(Deallocate: Boolean);
begin
if Deallocate then
Self.BeforeDestruction;
// Delphi doesn't really call the destructor recursively, but
// this is where the destructor's real work takes place.
Self.Destroy(False);
if Deallocate then
begin
// Delphi doesn't really call CleanupInstance. Instead, the
// FreeInstance method does the cleanup. If you override
// FreeInstance and do not call the inherited FreeInstance,
// you must call CleanupInstance to clean up strings,
// dynamic arrays, and Variant-type fields.
Self.CleanupInstance;
// Call FreeInstance to free the object's memory.
Self.FreeInstance;
end;
end;
Access Levels
Like C++ and Java, Delphi has different access levels that determine which objects can access the fields, methods, and properties of another object. The access levels are as follows:
private
Declarations that are declared private can be accessed only by the class's own methods or by any method, procedure, or function defined in the same unit's implementation section. Delphi does not have C++-style friend declarations or Java-style package level access. The equivalent in Delphi is to declare package or friend classes in the same unit, which gives them access to the private and protected parts of every class defined in the same unit.
protected
A protected declaration can be accessed from any method of the class or its descendants. The descendent classes can reside in different units.
public
Public methods have unrestricted access. Any method, function, or procedure can access a public declaration. Unless you use the $M+ compiler directive (see Chapter 8, Compiler Directives, for details), the default access level is public.
published
Published declarations are similar to public declarations, except that Delphi stores runtime type information for published declarations. Some declarations cannot be published; see Chapter 3 for details. If a class or a base class uses the $M+ directive, the default access level is published.
TIP:
Delphi's IDE declares fields and methods in the initial unnamed section of a form declaration. Because TForm inherits from TPersistent, which uses the $M+ directive, the initial section is published. In other words, the IDE declares its fields and methods as published. When Delphi loads a form description (.dfm file), it relies on the published information to build the form object. The IDE relies on the initial, unnamed section of the form class. If you modify that section, you run the risk of disabling the IDE's form editor.
automated
Automated declarations are similar to public declarations, except that Delphi stores additional runtime type information to support OLE automation servers. Automated declarations are obsolete; you should use Delphi's type library editor instead, but for now, they remain a part of the language for backward compatibility. A future release of Delphi might eliminate them entirely. Chapter 3 describes automated declarations in more depth.
A derived class can increase the access level of a property by redeclaring the property under the new access level (e.g., change protected to public). You cannot decrease a property's access level, and you cannot change the visibility of a field or method. You can override a virtual method and declare the overridden method at the same or higher access level, but you cannot decrease the access level.
Hiding a Constructor
Sometimes, a class is not for public use, but is a helper class whose use is entirely subservient to another class. In that case, you probably want to make the constructors for the helper class private or protected, but this is tricky. TObject declares a public constructor: Create. Even though the helper class's constructors are private or protected, you can call the public Create constructor inherited from TObject.
Although you cannot change the access level of the inherited Create constructor, you can hide it with another public constructor. Because the derived constructor should not be called, it can raise an exception. For example:
type
TPublic = class;
TPrivateHelper = class
private
// TPublic is the only class allowed to
// call the real constructor:
constructor Create(Owner: TPublic);
overload;
public
// Hide TObject.Create, in case someone
// accidentally tries to create a
// TPrivateHelper instance.
constructor Create;
reintroduce; overload;
end;
TPublic = class
private
fHelper: TPrivateHelper;
public
constructor Create;
destructor Destroy;
end;
constructor TPrivateHelper.Create;
begin
raise Exception.Create('Programming error')
end;
constructor TPublic.Create;
begin
// This is the only place where
// TPrivateHelper is created.
fHelper := TPrivateHelper.Create(Self);
end;
Properties
A property looks like a field but can act like a method. Properties take the place of accessor and mutator methods (sometimes called getters and setters), but have much more flexibility and power. Properties are vital to Delphi's IDE, and you can also use properties in many other situations.
A property has a reader and writer to get and set the property's value. The reader can be the name of a field, a selector for an aggregate field, or a method that returns the property value. The writer can be a field name, a selector for an aggregate field, or a method that sets the property value. You can omit the writer to make a read-only property. You can also omit the reader to create a write-only property, but the uses for such a beast are limited. Omitting both the reader and the writer is pointless, so Delphi does not let you do so.
Most readers and writers are field names or method names, but you can also refer to part of an aggregate field (record or array). If a reader or writer refers to an array element, the array index must be a constant, and the field's type cannot be a dynamic array. Records and arrays can be nested, and you can even use variant records. Example 2-9 shows an extended rectangle type, similar to the Windows TRect type, but because it is a class, it has properties and methods.
Example 2-9: Properties Readers and Writers
TRectEx = class(TPersistent)
private
R: TRect;
function GetHeight: Integer;
function GetWidth: Integer;
procedure SetHeight(const Value: Integer);
procedure SetWidth(const Value: Integer);
public
constructor Create(const R: TRect); overload;
constructor Create(Left, Top, Right, Bottom: Integer); overload;
constructor Create(const TopLeft, BottomRight: TPoint); overload;
procedure Assign(Source: TPersistent); override;
procedure Inflate(X, Y: Integer);
procedure Intersect(const R: TRectEx);
function IsEmpty: Boolean;
function IsEqual(const R: TRectEx): Boolean;
procedure Offset(X, Y: Integer);
procedure Union(const R: TRectEx);
property TopLeft: TPoint read R.TopLeft write R.TopLeft;
property BottomRight: TPoint read R.BottomRight write R.BottomRight;
property Rect: TRect read R write R;
property Height: Integer read GetHeight write SetHeight;
property Width: Integer read GetWidth write SetWidth;
published
property Left: Integer read R.Left write R.Left default 0;
property Right: Integer read R.Right write R.Right default 0;
property Top: Integer read R.Top write R.Top default 0;
property Bottom: Integer read R.Bottom write R.Bottom default 0;
end;
Array properties
Properties come in scalar and array flavors. An array property cannot be published, but they have many other uses. The array index can be any type, and you can have multidimensional arrays, too. For array-type properties, you must use read and write methods--you cannot map an array-type property directly to an array-type field.
You can designate one array property as the default property. You can refer to the default property by using an object reference and an array subscript without mentioning the property name, as shown in Example 2-10.
Example 2-10: Using a Default Array Property
type
TExample = class
...
property Items[I: Integer]: Integer read GetItem write SetItem;
property Chars[C: Char]: Char read GetChar write SetChar; default;
end;
var
Example: TExample;
I: Integer;
C: Char;
begin
Example := TExample.Create;
I := Example.Items[4]; // Must mention property name explicitly
C := Example['X']; // Array property is default
C := Example.Chars['X']; // Same as previous line
Indexed properties
You can map many properties to a single read or write method by specifying an index number for each property. The index value is passed to the read and write methods to differentiate one property from another.
You can even mix array indices and an index specifier. The reader and writer methods take the array indices as the first arguments, followed by the index specifier.
Default values
A property can also have stored and default directives. This information has no semantic meaning to the Delphi Pascal language, but Delphi's IDE uses this information when storing form descriptions. The value for the stored directive is a Boolean constant, a field of Boolean type, or a method that takes no arguments and returns a Boolean result. The value for the default directive is a constant value of the same type as the property. Only enumerated, integer, and set-type properties can have a default value. The stored and default directives have meaning only for published properties.
To distinguish a default array from a default value, the default array directive comes after the semicolon that ends the property declaration. The default value directive appears as part of the property declaration. See the default directive in Chapter 5 for details.
Using propertiesA common approach to writing Delphi classes is to make all fields private, and declare public properties to access the fields. Delphi imposes no performance penalty for properties that access fields directly. By using properties you get the added benefit of being able to change the implementation at a future date, say to add validation when a field's value changes. You can also use properties to enforce restricted access, such as using a read-only property to access a field whose value should not be changed. Example 2-11 shows some of the different ways to declare and use properties.
Example 2-11: Declaring and Using Properties
type
TCustomer = record
Name: string;
TaxIDNumber: string[9];
end;
TAccount = class
private
fCustomer: TCustomer;
fBalance: Currency;
fNumber: Cardinal;
procedure SetBalance(NewBalance: Currency);
published
property Balance: Currency read fBalance write SetBalance;
property Number: Cardinal read fNumber; // Cannot change account #
property CustName: string read fCustomer.Name;
end;
TSavingsAccount = class(TAccount)
private
fInterestRate: Integer;
published
property InterestRate: Integer read fInterestRate
write fInterestRate default DefaultInterestRate;
end;
TLinkedAccount = class(TObject)
private
fAccounts: array[0..1] of TAccount;
function GetAccount(Index: Integer): TAccount;
public
// Two ways for properties to access an array: using an index
// or referring to an array element.
property Checking: TAccount index 0 read GetAccount;
property Savings: TAccount read fAccounts[1];
end;
TAccountList = class
private
fList: TList;
function GetAccount(Index: Integer): TAccount;
procedure SetAccount(Index: Integer; Account: TAccount);
function GetCount: Integer;
protected
property List: TList read fList;
public
property Count: Integer read GetCount;
property Accounts[Index: Integer]: TAccount read GetAccount
write SetAccount; default;
end;
procedure TAccount.SetBalance(NewBalance: Currency);
begin
if NewBalance < 0 then
raise EOverdrawnException.Create;
fBalance := NewBalance;
end;
function TLinkedAccount.GetAccount(Index: Integer): TAccount;
begin
Result := fAccounts[Index]
end;
function TAccountList.GetCount: Integer;
begin
Result := List.Count
end;
function TAccountList.GetAccount(Index: Integer): TAccount;
begin
Result := List[Index]
end;
procedure TAccountList.SetAccount(Index: Integer; Account: TAccount);
begin
fList[Index] := Account
end;
Class-type properties
Properties of class type need a little extra attention. The best way to work with class-type properties is to make sure the owner object manages the property object. In other words, don't save a reference to other objects, but keep a private copy of the property object. Use a write method to store an object by copying it. Delphi's IDE requires this behavior of published properties, and it makes sense for unpublished properties, too.
The only exception to the rule for class-type properties is when a property stores a reference to a component on a form. In that case, the property must store an object reference and not a copy of the component.
Delphi's IDE stores component references in a .dfm file by storing only the component name. When the .dfm is loaded, Delphi looks up the component name to restore the object reference. If you must store an entire component within another component, you must delegate all properties of the inner component.
Make sure the property's class inherits from TPersistent and that the class overrides the Assign method. Implement your property's write method to call Assign. (TPersistent--in the Classes unit--is not required, but it's the easiest way to copy an object. Otherwise, you need to duplicate the Assign method in whatever class you use.) The read method can provide direct access to the field. If the property object has an OnChange event, you might need to set that so your object is notified of any changes. Example 2-12 shows a typical pattern for using a class-type property. The example defines a graphical control that repeatedly displays a bitmap throughout its extent, tiling the bitmap as necessary. The Bitmap property stores a TBitmap object.
Example 2-12: Declaring and Using a Class-type Property
unit Tile; interface uses SysUtils, Classes, Controls, Graphics; type // Tile a bitmap TTile = class(TGraphicControl) private fBitmap: TBitmap; procedure SetBitmap(NewBitmap: TBitmap); procedure BitmapChanged(Sender: TObject); protected procedure Paint; override; public constructor Create(Owner: TComponent); override; destructor Destroy; override; published property Align; property Bitmap: TBitmap read fBitmap write SetBitmap; property OnClick; property OnDblClick; // Many other properties are useful, but were omitted to save space. // See TControl for a full list. end; implementation { TTile } // Create the bitmap when creating the control. constructor TTile.Create(Owner: TComponent); begin inherited; fBitmap := TBitmap.Create; fBitmap.OnChange := BitmapChanged; end; // Free the bitmap when destroying the control. destructor TTile.Destroy; begin FreeAndNil(fBitmap); inherited; end; // When the bitmap changes, redraw the control. procedure TTile.BitmapChanged(Sender: TObject); begin Invalidate; end; // Paint the control by tiling the bitmap. If there is no // bitmap, don't paint anything. procedure TTile.Paint; var X, Y: Integer; begin if (Bitmap.Width = 0) or (Bitmap.Height = 0) then Exit; Y := 0; while Y < ClientHeight do begin X := 0; while X < ClientWidth do begin Canvas.Draw(X, Y, Bitmap); Inc(X, Bitmap.Width); end; Inc(Y, Bitmap.Height); end; end; // Set a new bitmap by copying the TBitmap object. procedure TTile.SetBitmap(NewBitmap: TBitmap); begin fBitmap.Assign(NewBitmap); end; end.
Interfaces
An interface defines a type that comprises abstract virtual methods. Although a class inherits from a single base class, it can implement any number of interfaces. An interface is similar to an abstract class (that is, a class that has no fields and all of whose methods are abstract), but Delphi has extra magic to help you work with interfaces. Delphi's interfaces sometimes look like COM (Component Object Model) interfaces, but you don't need to know COM to use Delphi interfaces, and you can use interfaces for many other purposes.
You can declare a new interface by inheriting from an existing interface. An interface declaration contains method and property declarations, but no fields. Just as all classes inherit from TObject, all interfaces inherit from IUnknown. The IUnknown interface declares three methods: _AddRef, _Release, and QueryInterface. If you are familiar with COM, you will recognize these methods. The first two methods manage reference counting for the lifetime of the object that implements the interface. The third method accesses other interfaces an object might implement.
When you declare a class that implements one or more interfaces, you must provide an implementation of all the methods declared in all the interfaces. The class can implement an interface's methods, or it can delegate the implementation to a property, whose value is an interface. The simplest way to implement the _AddRef, _Release, and QueryInterface methods is to inherit them from TInterfacedObject or one of its derived classes, but you are free to inherit from any other class if you wish to define the methods yourself.
A class implements each of an interface's methods by declaring a method with the same name, arguments, and calling convention. Delphi automatically matches the class's methods with the interface's methods. If you want to use a different method name, you can redirect an interface method to a method with a different name. The redirected method must have the same arguments and calling convention as the interface method. This feature is especially important when a class implements multiple interfaces with identical method names. See the class keyword in Chapter 5 for more information about redirecting methods.
A class can delegate the implementation of an interface to a property that uses the implements directive. The property's value must be the interface that the class wants to implement. When the object is cast to that interface type, Delphi automatically fetches the property's value and returns that interface. See the implements directive in Chapter 5 for details.
For each non-delegated interface, the compiler creates a hidden field to store a pointer to the interface's VMT. The interface field or fields follow immediately after the object's hidden VMT field. Just as an object reference is really a pointer to the object's hidden VMT field, an interface reference is a pointer to the interface's hidden VMT field. Delphi automatically initializes the hidden fields when the object is constructed. See Chapter 3 to learn how the compiler uses RTTI to keep track of the VMT and the hidden field.
Reference counting
The compiler generates calls to _AddRef and _Release to manage the lifetime of interfaced objects. To use Delphi's automatic reference counting, declare a variable with an interface type. When you assign an interface reference to an interface variable, Delphi automatically calls _AddRef. When the variable goes out of scope, Delphi automatically calls _Release.
The behavior of _AddRef and _Release is entirely up to you. If you inherit from TInterfacedObject, these methods implement reference counting. The _AddRef method increments the reference count, and _Release decrements it. When the reference count goes to zero, _Release frees the object. If you inherit from a different class, you can define these methods to do anything you want. You should implement QueryInterface correctly, though, because Delphi relies on it to implement the as operator.
Typecasting
Delphi calls QueryInterface as part of its implementation of the as operator for interfaces. You can use the as operator to cast an interface to any other interface type. Delphi calls QueryInterface to obtain the new interface reference. If QueryInterface returns an error, the as operator raises a runtime error. (The SysUtils unit maps the runtime error to an EIntfCastError exception.)
You can implement QueryInterface any way you want, but you probably want to use the same approach taken by TInterfacedObject. Example 2-13 shows a class that implements QueryInterface normally, but uses stubs for _AddRef and _Release. Later in this section, you'll see how useful this class can be.
Example 2-13: Interface Base Class Without Reference Counting
type
TNoRefCount = class(TObject, IUnknown)
protected
function QueryInterface(const IID:TGUID; out Obj):HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
function TNoRefCount.QueryInterface(const IID:TGUID; out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := Windows.E_NoInterface;
end;
function TNoRefCount._AddRef: Integer;
begin
Result := -1
end;
function TNoRefCount._Release: Integer;
begin
Result := -1
end;
Interfaces and object-oriented programming
The most important use of interfaces is to separate type inheritance from class inheritance. Class inheritance is an effective tool for code reuse. A derived class easily inherits the fields, methods, and properties of a base class, and thereby avoids reimplementing common methods. In a strongly typed language, such as Delphi, the compiler treats a class as a type, and therefore class inheritance becomes synonymous with type inheritance. In the best of all possible worlds, though, types and classes are entirely separate.
Textbooks on object-oriented programming often describe an inheritance relationship as an "is-a" relationship, for example, a TSavingsAccount "is-a" TAccount. You can see the same idea in Delphi's is operator, where you test whether an Account variable is TSavingsAccount.
Outside of textbook examples, though, simple is-a relationships break down. A square is a rectangle, but that doesn't mean you want to derive TSquare from TRectangle. A rectangle is a polygon, but you probably don't want to derive TRectangle from TPolygon. Class inheritance forces a derived class to store all the fields that are declared in the base class, but in this case, the derived class doesn't need that information. A TSquare object can get away with storing a single length for all of its sides. A TRectangle object, however, must store two lengths. A TPolygon object needs to store many sides and vertices.
The solution is to separate the type inheritance (a square is a rectangle is a polygon) from class inheritance (class C inherits the fields and methods of class B, which inherits the fields and methods of class A). Use interfaces for type inheritance, so you can leave class inheritance to do what it does best: inheriting fields and methods.
In other words, ISquare inherits from IRectangle, which inherits from IPolygon. The interfaces follow the "is-a" relationship. Entirely separate from the interfaces, the class TSquare implements ISquare, IRectangle, and IPolygon. TRectangle implements IRectangle and IPolygon.
TIP:
The convention in COM programming is to name interfaces with an initial I. Delphi follows this convention for all interfaces. Note that it is a useful convention, but not a language requirement.
On the implementation side, you can declare additional classes to implement code reuse. For example, TBaseShape implements the common methods and fields for all shapes. TRectangle inherits from TBaseShape and implements the methods in a way that make sense for rectangles. TPolygon also inherits from TBaseShape and implements the methods in a way that make sense for other kinds of polygons.
A drawing program can use the shapes by manipulating IPolygon interfaces. Example 2-14 shows simplified classes and interfaces for this scheme. Notice how each interface has a GUID (Globally Unique Identifier) in its declaration. The GUID is necessary for using QueryInterface. If you need the GUID of an interface (in an explicit call to QueryInterface, for example), you can use the interface name. Delphi automatically converts an interface name to its GUID.
Example 2-14: Separating Type and Class Hierarchies
type
IShape = interface
['{50F6D851-F4EB-11D2-88AC-00104BCAC44B}']
procedure Draw(Canvas: TCanvas);
function GetPosition: TPoint;
procedure SetPosition(Value: TPoint);
property Position: TPoint read GetPosition write SetPosition;
end;
IPolygon = interface(IShape)
['{50F6D852-F4EB-11D2-88AC-00104BCAC44B}']
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
IRectangle = interface(IPolygon)
['{50F6D853-F4EB-11D2-88AC-00104BCAC44B}']
end;
ISquare = interface(IRectangle)
['{50F6D854-F4EB-11D2-88AC-00104BCAC44B}']
function Side: Integer;
end;
TBaseShape = class(TNoRefCount, IShape)
private
fPosition: TPoint;
function GetPosition: TPoint;
procedure SetPosition(Value: TPoint);
public
constructor Create; virtual;
procedure Draw(Canvas: TCanvas); virtual; abstract;
property Position: TPoint read fPosition write SetPosition;
end;
TPolygon = class(TBaseShape, IPolygon)
private
fVertices: array of TPoint;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TRectangle = class(TBaseShape, IPolygon, IRectangle)
private
fRect: TRect;
public
procedure Draw(Canvas: TCanvas); override;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
TSquare = class(TBaseShape, IPolygon, IRectangle, ISquare)
private
fSide: Integer;
public
procedure Draw(Canvas: TCanvas); override;
function Side: Integer;
function NumVertices: Integer;
function NumSides: Integer;
function SideLength(Index: Integer): Integer;
function Vertex(Index: Integer): TPoint;
end;
A derived class inherits the interfaces implemented by the ancestors' classes. Thus, TRectangle inherits from TBaseShape, and TBaseShape implements IShape so TRectangle implements IShape. Inheritance of interfaces works a little differently. Interface inheritance is merely a typing convenience, so you don't have to retype a lot of method declarations. When a class implements an interface, that does not automatically mean the class implements the ancestor interfaces. A class implements only those interfaces that are listed in its class declaration (and in the declaration for ancestor classes). Thus, even though IRectangle inherits from IPolygon, the TRectangle class must list IRectangle and IPolygon explicitly.
To implement a type hierarchy, you might not want to use reference counting. Instead, you will rely on explicit memory management, the way you do for normal Delphi objects. In this case, it's best to implement the _AddRef and _Release methods as stubs, such as those in the TNoRefCount class in Example 2-13. Just be careful not to have any variables that hold stale references. A variable that refers to an object that has been freed can cause problems if you use the variable. An interface variable that refers to an object that has been freed will certainly cause problems, because Delphi will automatically call its _Release method. In other words, you never want to have variables that contain invalid pointers, and working with interfaces that do not use reference counting forces you to behave.
COM and Corba
Delphi interfaces are also useful for implementing and using COM and Corba objects. You can define a COM server that implements many interfaces, and Delphi automatically manages the COM aggregation for you. The runtime library contains many classes that make it easier to define COM servers, class factories, and so on. Because these classes are not part of the Delphi Pascal language, they are not covered in this book. Consult the product documentation to learn more.
Reference Counting
The previous section discusses how Delphi uses reference counting to manage the lifetime of interfaces. Strings and dynamic arrays also use reference counting to manage their lifetimes. The compiler generates appropriate code to keep track of when interface references, strings, and dynamic arrays are created and when the variables go out of scope and the objects, strings, and arrays must be destroyed.
Usually, the compiler can handle the reference counting automatically, and everything works the way the you expect it to. Sometimes, though, you need to give a hint to the compiler. For example, if you declare a record that contains a reference counted field, and you use GetMem to allocate a new instance of the record, you must call Initialize, passing the record as an argument. Before calling FreeMem, you must call Finalize.
Sometimes, you want to keep a reference to a string or interface after the variable goes out of scope, that is, at the end of the block where the variable is declared. For example, maybe you want to associate an interface with each item in a TListView. You can do this by explicitly managing the reference count. When storing the interface, be sure to cast it to IUnknown, call _AddRef, and cast the IUnknown reference to a raw pointer. When extracting the data, type cast the pointer to IUnknown. You can then use the as operator to cast the interface to any desired type, or just let Delphi release the interface. For convenience, declare a couple of subroutines to do the dirty work for you, and you can reuse these subroutines any time you need to retain an interface reference. Example 2-15 shows an example of how you can store an interface reference as the data associated with a list view item.
Example 2-15: Storing Interfaces in a List View
// Cast an interface to a Pointer such that the reference
// count is incremented and the interface will not be freed
// until you call ReleaseIUnknown.
function RefIUnknown(const Intf: IUnknown): Pointer;
begin
Intf._AddRef; // Increment the reference count.
Result := Pointer(Intf); // Save the interface pointer.
end;
// Release the interface whose value is stored in the pointer P.
procedure ReleaseIUnknown(P: Pointer);
var
Intf: IUnknown;
begin
Pointer(Intf) := P;
// Delphi releases the interface when Intf goes out of scope.
end;
// When the user clicks the button, add an interface to the list.
procedure TForm1.Button1Click(Sender: TObject);
var
Item: TListItem;
begin
Item := ListView1.Items.Add;
Item.Caption := 'Stuff';
Item.Data := RefIUnknown(GetIntf as IUnknown);
end;
// When the list view is destroyed or the list item is destroyed
// for any other reason, release the interface, too.
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
ReleaseIUnknown(Item.Data);
end;
// When the user selects the list view item, do something with the
// associated interface.
procedure TForm1.ListView1Click(Sender: TObject);
var
Intf: IMyInterface;
begin
Intf := IUnknown(ListView1.Selected.Data) as IMyInterface;
Intf.DoSomethingUseful;
end;
You can also store strings as data. Instead of using _AddRef, cast the string to a Pointer to store the reference to the string, then force the variable to forget about the string. When the variable goes out of scope, Delphi will not free the string, because the variable has forgotten all about it. After retrieving the pointer, assign it to a string variable that is cast to a pointer. When the subroutine returns, Delphi automatically frees the string's memory. Be sure your program does not retain any pointers to memory that is about to be freed. Again, convenience subroutines simplify the task. Example 2-16 shows one way to store strings.
Example 2-16: Storing Strings in a List View
// Save a reference to a string and return a raw pointer
// to the string.
function RefString(const S: string): Pointer;
var
Local: string;
begin
Local := S; // Increment the reference count.
Result := Pointer(Local); // Save the string pointer.
Pointer(Local) := nil; // Prevent decrementing the ref count.
end;
// Release a string that was referenced with RefString.
procedure ReleaseString(P: Pointer);
var
Local: string;
begin
Pointer(Local) := P;
// Delphi frees the string when Local goes out of scope.
end;
// When the user clicks the button, add an item to the list view
// and save an additional, hidden string.
procedure TForm1.Button1Click(Sender: TObject);
var
Item: TListItem;
begin
Item := ListView1.Items.Add;
Item.Caption := Edit1.Text;
Item.Data := RefString(Edit2.Text);
end;
// Release the string when the list view item is destroyed
// for any reason.
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
ReleaseString(Item.Data);
end;
// Retrieve the string when the user selects the list view item.
procedure TForm1.ListView1Click(Sender: TObject);
var
Str: string;
begin
if ListView1.Selected <> nil then
begin
Str := string(ListView1.Selected.Data);
ShowMessage(Str);
end;
end;